Improve teams settings invites and due tasks

This commit is contained in:
rucki
2026-08-23 17:50:21 +02:00
parent c409fbc441
commit c7f7bc801e
25 changed files with 1165 additions and 454 deletions
+2 -1
View File
@@ -1,5 +1,5 @@
from django.contrib import admin
from .models import ApiKey, Attachment, Item, Kanban, Tag, Team, TeamInvite, TeamMembership, UserPreference
from .models import ApiKey, Attachment, Item, Kanban, Tag, Team, TeamInvite, TeamMembership, UserInvite, UserPreference
class AttachmentInline(admin.TabularInline):
@@ -22,6 +22,7 @@ class ApiKeyAdmin(admin.ModelAdmin):
admin.site.register(Tag)
admin.site.register(UserInvite)
admin.site.register(Team)
admin.site.register(TeamMembership)
admin.site.register(TeamInvite)
+24
View File
@@ -0,0 +1,24 @@
from django.urls import reverse
from django.utils.translation import gettext as _
from .models import UserPreference
def overview_navigation(request):
if not request.user.is_authenticated:
return {'nav_overview_labels': [_('Notes'), _('Tasks'), _('Links')], 'nav_hidden_links': []}
prefs, _created = UserPreference.objects.get_or_create(user=request.user)
overview = []
hidden = []
task_shown = prefs.show_open_todos or prefs.show_done_todos
items = [
(task_shown, _('Tasks'), f"{reverse('index')}?kind=todo"),
(prefs.show_notes, _('Notes'), f"{reverse('index')}?kind=note"),
(prefs.show_links, _('Links'), f"{reverse('index')}?kind=link"),
]
for shown, label, url in items:
if shown:
overview.append(label)
else:
hidden.append({'label': label, 'url': url})
return {'nav_overview_labels': overview or [_('Overview')], 'nav_hidden_links': hidden}
+32 -10
View File
@@ -1,7 +1,8 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from .models import ApiKey, Attachment, Item, Kanban, Tag, Team, TeamInvite, UserPreference
from .models import ApiKey, Attachment, Item, Kanban, Tag, Team, TeamInvite, UserInvite, UserPreference
class MultipleFileInput(forms.ClearableFileInput):
@@ -38,17 +39,12 @@ class EditItemForm(forms.ModelForm):
class Meta:
model = Item
fields = ['content', 'comment', 'visibility', 'due_at', 'url']
fields = ['content', 'comment', 'visibility', 'url']
widgets = {
'comment': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'due_at': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%dT%H:%M'),
'url': forms.URLInput(attrs={'class': 'form-control', 'placeholder': 'URL'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['due_at'].input_formats = ['%Y-%m-%dT%H:%M']
class UserSettingsForm(forms.ModelForm):
class Meta:
@@ -60,16 +56,42 @@ class UserSettingsForm(forms.ModelForm):
class UserPreferenceForm(forms.ModelForm):
class Meta:
model = UserPreference
fields = ['show_notes', 'show_open_todos', 'show_done_todos', 'show_links', 'show_journal', 'overview_lines']
fields = ['language', 'show_notes', 'show_open_todos', 'show_done_todos', 'show_links', 'due_first', 'overview_lines']
labels = {
'language': _('Language'),
'show_notes': _('Show notes'),
'show_open_todos': _('Show open tasks'),
'show_done_todos': _('Show completed tasks'),
'show_links': _('Show links'),
'show_journal': _('Show journal'),
'due_first': _('Always show due items first'),
'overview_lines': _('Lines in overview'),
}
widgets = {'overview_lines': forms.NumberInput(attrs={'class': 'form-control', 'min': 1, 'max': 50})}
widgets = {
'language': forms.Select(attrs={'class': 'form-select'}),
'overview_lines': forms.NumberInput(attrs={'class': 'form-control', 'min': 1, 'max': 50}),
}
class UserInviteForm(forms.ModelForm):
email = forms.EmailField(required=True, label=_('Email'), widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'person@example.com', 'required': True}))
class Meta:
model = UserInvite
fields = ['email']
class InviteRegistrationForm(UserCreationForm):
email = forms.EmailField(required=False, label=_('Email'), widget=forms.EmailInput(attrs={'class': 'form-control'}))
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
widgets = {'username': forms.TextInput(attrs={'class': 'form-control'})}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name in ['password1', 'password2']:
self.fields[name].widget.attrs.update({'class': 'form-control'})
class TeamForm(forms.ModelForm):
+24
View File
@@ -0,0 +1,24 @@
from django.utils import translation
class UserLanguageMiddleware:
"""Prefer the authenticated user's saved language over browser/cookie language."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
user = getattr(request, 'user', None)
language = ''
if user and user.is_authenticated:
try:
language = user.preferences.language
except Exception:
language = ''
if language:
translation.activate(language)
request.LANGUAGE_CODE = language
response = self.get_response(request)
if language:
response.setdefault('Content-Language', language)
return response
@@ -0,0 +1,18 @@
# Generated by Django 5.2.17 on 2026-08-23 13:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0011_alter_item_kanban_status_alter_item_kind_and_more'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='language',
field=models.CharField(blank=True, choices=[('', 'Browser default'), ('de', 'Deutsch'), ('en', 'English')], default='', max_length=10),
),
]
+30
View File
@@ -0,0 +1,30 @@
# Generated by Django 5.2.17 on 2026-08-23 13:53
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0012_userpreference_language'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserInvite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(blank=True, max_length=254)),
('token', models.CharField(editable=False, max_length=80, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('accepted_at', models.DateTimeField(blank=True, null=True)),
('invited_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_user_invites', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.17 on 2026-08-23 14:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0013_userinvite'),
]
operations = [
migrations.AlterField(
model_name='userinvite',
name='email',
field=models.EmailField(max_length=254),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.17 on 2026-08-23 14:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_alter_userinvite_email'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='due_first',
field=models.BooleanField(default=True),
),
]
+30
View File
@@ -3,6 +3,7 @@ import secrets
from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
TAG_RE = re.compile(r'(?<!\w)#([\wäöüÄÖÜß-]+)')
@@ -19,6 +20,25 @@ class Tag(models.Model):
return f'#{self.name}'
class UserInvite(models.Model):
email = models.EmailField()
token = models.CharField(max_length=80, unique=True, editable=False)
invited_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='sent_user_invites')
created_at = models.DateTimeField(auto_now_add=True)
accepted_at = models.DateTimeField(blank=True, null=True)
class Meta:
ordering = ['-created_at']
def save(self, *args, **kwargs):
if not self.token:
self.token = secrets.token_urlsafe(32)
super().save(*args, **kwargs)
def __str__(self):
return f'Benutzereinladung {self.email}'
class Team(models.Model):
name = models.CharField(max_length=120)
slug = models.SlugField(max_length=80, unique=True, allow_unicode=True, help_text='Team-Tag ohne #, z.B. my2dos')
@@ -112,6 +132,14 @@ class Item(models.Model):
def get_absolute_url(self):
return reverse('item_detail', args=[self.pk])
@property
def is_overdue(self):
return self.kind == self.Kind.TODO and not self.is_done and self.due_at and self.due_at < timezone.now()
@property
def is_due_today(self):
return self.kind == self.Kind.TODO and not self.is_done and self.due_at and timezone.localdate(self.due_at) == timezone.localdate()
def sync_metadata(self):
tag_names = {m.group(1).lower() for m in TAG_RE.finditer(self.content)}
tags = [Tag.objects.get_or_create(name=name)[0] for name in tag_names]
@@ -136,11 +164,13 @@ class Kanban(models.Model):
class UserPreference(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='preferences')
language = models.CharField(max_length=10, blank=True, default='', choices=[('', _('Browser default')), ('de', 'Deutsch'), ('en', 'English')])
show_notes = models.BooleanField(default=True)
show_open_todos = models.BooleanField(default=True)
show_done_todos = models.BooleanField(default=False)
show_links = models.BooleanField(default=True)
show_journal = models.BooleanField(default=False)
due_first = models.BooleanField(default=True)
overview_lines = models.PositiveSmallIntegerField(default=4)
def __str__(self):
+1 -1
View File
@@ -20,7 +20,7 @@
<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>
{% if item.kind == 'todo' %}<div class="col-md-4"><input class="form-control" type="datetime-local" name="due_at" value="{{ item.due_at|date:'Y-m-d\\TH:i' }}" title="{% trans 'Date/time for todo' %}"></div>{% endif %}
{% 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>
{% if item.kind == 'link' %}<input class="form-control" type="url" name="url" value="{{ item.url }}" placeholder="URL">{% endif %}
+2 -2
View File
@@ -1,5 +1,5 @@
{% load markdown_extras i18n %}
<article id="item-{{ item.id }}" class="card item-card shadow-sm {% if item.is_done %}done{% endif %}" ondblclick="if(!event.target.closest('a,button,input,textarea,select,label')) window.location.href='{{ item.get_absolute_url }}?next={{ request.get_full_path|urlencode }}'">
<article id="item-{{ item.id }}" class="card item-card shadow-sm {% if item.is_done %}done{% elif item.is_overdue %}border-danger border-2{% elif item.is_due_today %}border-warning border-2{% endif %}" ondblclick="if(!event.target.closest('a,button,input,textarea,select,label')) window.location.href='{{ item.get_absolute_url }}?next={{ request.get_full_path|urlencode }}'">
<div class="card-body">
<div class="item-row d-flex justify-content-between align-items-start gap-3">
<div class="item-main flex-grow-1 min-w-0" style="min-width:0">
@@ -14,7 +14,7 @@
{% if overview_lines %}<button x-show="showMore" type="button" class="btn btn-link btn-sm p-0 mt-1" @click="expanded=!expanded" x-text="expanded ? '{% trans "show less" %}' : '{% trans "show more" %}'"></button>{% endif %}
</div>
{% if item.comment %}<div class="border-start ps-2 mt-2 small content text-muted">{{ item.comment|markdown }}</div>{% endif %}
{% if item.due_at %}<div class="small text-warning-emphasis mt-2">{% trans "Due:" %} {{ item.due_at|date:'d.m.Y H:i' }}</div>{% endif %}
{% if item.due_at %}<div class="small mt-2 {% if item.is_overdue %}text-danger fw-semibold{% elif item.is_due_today %}text-warning-emphasis fw-semibold{% else %}text-warning-emphasis{% endif %}">{% if item.is_overdue %}<span class="badge text-bg-danger me-1">{% trans "Overdue" %}</span>{% elif item.is_due_today %}<span class="badge text-bg-warning me-1">{% trans "Due today" %}</span>{% endif %}{% trans "Due:" %} {{ item.due_at|date:'d.m.Y H:i' }}</div>{% endif %}
{% if item.url %}<a href="{{ item.url }}" target="_blank" rel="noopener">{{ item.url }}</a>{% endif %}
{% if item.linked_items.all %}<div class="small mt-2">{% trans "Links:" %} {% for linked in item.linked_items.all %}<a href="{{ linked.get_absolute_url }}">#{{ linked.id }}</a> {% endfor %}</div>{% endif %}
{% if item.attachments.all %}
+3 -3
View File
@@ -1,10 +1,10 @@
{% load querystring i18n %}
<div id="tag-list" class="d-flex flex-wrap gap-2"{% if oob %} hx-swap-oob="true"{% endif %}>
{% for tag in tags %}
{% if active_tag == tag.name %}
<a class="badge rounded-pill text-bg-dark text-decoration-none tag" href="{% query_replace tag=None %}">#{{ tag.name }} ×</a>
{% if tag.name in active_tags %}
<a class="badge rounded-pill text-bg-dark text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" href="{% query_replace tag=None %}">#{{ tag.name }} ×</a>
{% else %}
<a class="badge rounded-pill text-bg-light text-decoration-none tag" href="{% query_replace tag=tag.name %}">#{{ tag.name }}</a>
<a class="badge rounded-pill text-bg-light text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" href="{% query_replace tag=tag.name %}">#{{ tag.name }}</a>
{% endif %}
{% empty %}<span class="text-muted small">{% trans "No tags yet" %}</span>{% endfor %}
</div>
@@ -0,0 +1,23 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% block content %}
<div class="row justify-content-center"><div class="col-md-6">
<div class="card item-card shadow-sm"><div class="card-body">
<h1 class="h4 mb-2">{% trans "Create account" %}</h1>
<p class="text-muted">{% trans "You were invited to my2dos. Create your account to continue." %}</p>
<form method="post" class="vstack gap-3">
{% csrf_token %}
{% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
{% for field in form %}
<div>
<label class="form-label">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}<div class="form-text">{{ field.help_text }}</div>{% endif %}
{% for error in field.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
</div>
{% endfor %}
<button class="btn btn-dark">{% trans "Create account" %}</button>
</form>
</div></div>
</div></div>
{% endblock %}
+3 -2
View File
@@ -17,11 +17,11 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
body{background:#f7f7f4}.shell{max-width:960px}.item-card{border:0;border-radius:1rem}.content p:last-child{margin-bottom:0}.tag{font-size:.85rem}.slash-menu{position:absolute;z-index:20;top:100%;left:0;width:18rem}.done{opacity:.6}.done .content{text-decoration:line-through}.content img{max-width:100%;max-height:420px;border-radius:.5rem;border:1px solid #ddd;padding:.25rem;background:white}.overview-content{max-height:calc(var(--overview-lines) * 1.55em);overflow:hidden}.overview-content.expanded{max-height:none}.content{min-width:0;max-width:100%;overflow-wrap:anywhere}.content pre{position:relative;display:block;width:100%;max-width:100%;box-sizing:border-box;background:#1f2937;color:#f9fafb;border-radius:.6rem;padding:1rem;overflow-x:auto;overflow-y:auto;white-space:pre}.content pre code{display:block;color:inherit;white-space:pre;min-width:0}.copy-code{position:absolute;top:.4rem;right:.4rem;line-height:1}.mobile-create-buttons,.mobile-create-nav,.mobile-tags,.mobile-media,.mobile-tag-picker{display:none}.mobile-tag-popup{position:fixed;inset:auto .75rem 1rem .75rem;z-index:1050;max-height:55vh;overflow:auto}.mobile-tag-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.25);z-index:1040}.camera-input{max-width:13rem}@media (max-width: 767.98px){.shell{max-width:100%}main.container{padding-left:.75rem;padding-right:.75rem}.navbar .container{gap:.5rem}.navbar-brand{font-size:1rem}.navbar .d-flex{gap:.5rem!important}.item-card{border-radius:.75rem}.mobile-create-buttons,.mobile-create-nav,.mobile-tags,.mobile-media{display:flex!important}.mobile-tag-picker{display:block!important}.desktop-quick-create{display:none!important}.mobile-create-nav{position:sticky;top:57px;z-index:10;background:#f7f7f4;padding:.35rem 0}.desktop-file{display:none!important}.mobile-create-buttons{position:sticky;top:57px;z-index:10;background:#f7f7f4;padding:.35rem 0}.quick-actions{flex-wrap:wrap}.quick-actions .form-control{min-width:100%}.slash-menu{width:calc(100vw - 1.5rem)}textarea.form-control-lg{font-size:1rem}.btn{touch-action:manipulation}.item-row{flex-direction:column}.item-actions{align-self:flex-end}.item-main{width:100%;min-width:0}}
body{background:linear-gradient(180deg,#f8fafc 0,#f7f7f4 18rem);color:#1f2937}.shell{max-width:960px}.navbar{box-shadow:0 .35rem 1.25rem rgba(15,23,42,.06)}.navbar a{text-decoration:none}.nav-soft{color:#475569!important;font-weight:500;padding:.2rem .35rem;border-radius:.5rem}.nav-soft:hover{color:#0d6efd!important;background:#eff6ff}.user-menu-btn{min-width:9rem}.card{border:0}.item-card{border:0;border-radius:1.1rem;box-shadow:0 .45rem 1.4rem rgba(15,23,42,.07)!important}.filter-panel{padding:.25rem .15rem}.border{border-color:rgba(15,23,42,.10)!important}.text-bg-light{background:#f1f5f9!important;color:#475569!important}.btn{border-radius:.7rem}.form-control,.form-select{border-color:#e2e8f0;border-radius:.75rem}.content p:last-child{margin-bottom:0}.tag{font-size:.85rem}.slash-menu{position:absolute;z-index:20;top:100%;left:0;width:18rem}.done{opacity:.6}.done .content{text-decoration:line-through}.content img{max-width:100%;max-height:420px;border-radius:.5rem;border:1px solid #ddd;padding:.25rem;background:white}.overview-content{max-height:calc(var(--overview-lines) * 1.55em);overflow:hidden}.overview-content.expanded{max-height:none}.content{min-width:0;max-width:100%;overflow-wrap:anywhere}.content pre{position:relative;display:block;width:100%;max-width:100%;box-sizing:border-box;background:#1f2937;color:#f9fafb;border-radius:.6rem;padding:1rem;overflow-x:auto;overflow-y:auto;white-space:pre}.content pre code{display:block;color:inherit;white-space:pre;min-width:0}.copy-code{position:absolute;top:.4rem;right:.4rem;line-height:1}.mobile-create-buttons,.mobile-create-nav,.mobile-tags,.mobile-media,.mobile-tag-picker{display:none}.mobile-tag-popup{position:fixed;inset:auto .75rem 1rem .75rem;z-index:1050;max-height:55vh;overflow:auto}.mobile-tag-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.25);z-index:1040}.camera-input{max-width:13rem}@media (max-width: 767.98px){.shell{max-width:100%}main.container{padding-left:.75rem;padding-right:.75rem}.navbar .container{gap:.5rem}.navbar-brand{font-size:1rem}.navbar .d-flex{gap:.5rem!important}.item-card{border-radius:.75rem}.mobile-create-buttons,.mobile-create-nav,.mobile-tags,.mobile-media{display:flex!important}.mobile-tag-picker{display:block!important}.desktop-quick-create{display:none!important}.mobile-create-nav{position:sticky;top:57px;z-index:10;background:#f7f7f4;padding:.35rem 0}.desktop-file{display:none!important}.mobile-create-buttons{position:sticky;top:57px;z-index:10;background:#f7f7f4;padding:.35rem 0}.quick-actions{flex-wrap:wrap}.quick-actions .form-control{min-width:100%}.slash-menu{width:calc(100vw - 1.5rem)}textarea.form-control-lg{font-size:1rem}.btn{touch-action:manipulation}.item-row{flex-direction:column}.item-actions{align-self:flex-end}.item-main{width:100%;min-width:0}}
</style>
</head>
<body>
<nav class="navbar bg-white border-bottom sticky-top"><div class="container shell"><a class="navbar-brand fw-bold" href="/">my2dos</a><div class="d-flex align-items-center gap-3"><a class="small d-none d-sm-inline text-primary text-decoration-underline" href="/">{% trans "Notes · Tasks · Links" %}</a>{% if user.is_authenticated %}<a class="small" href="{% url 'journal' %}">{% trans "Journal" %}</a><a class="small" href="{% url 'kanban_list' %}">{% trans "Kanban" %}</a><a class="small" href="{% url 'settings' %}#teams">{% trans "Teams" %}</a><div class="dropdown"><button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">{{ user.get_full_name|default:user.username }}</button><ul class="dropdown-menu dropdown-menu-end"><li><a class="dropdown-item" href="{% url 'settings' %}">{% trans "Settings" %}</a></li><li><button id="pwa-install" type="button" class="dropdown-item d-none">{% trans "Install app" %}</button></li>{% if user.is_staff %}<li><a class="dropdown-item" href="/admin/">Admin</a></li>{% endif %}<li><hr class="dropdown-divider"></li><li><form method="post" action="{% url 'logout' %}">{% csrf_token %}<button class="dropdown-item">{% trans "Logout" %}</button></form></li></ul></div>{% else %}<a class="btn btn-sm btn-dark" href="{% url 'login' %}">{% trans "Login" %}</a>{% endif %}<form action="{% url 'set_language' %}" method="post" class="d-flex align-items-center">{% csrf_token %}<input name="next" type="hidden" value="{{ request.get_full_path }}"><select name="language" class="form-select form-select-sm" onchange="this.form.submit()">{% get_current_language as LANGUAGE_CODE %}{% get_available_languages as LANGUAGES %}{% for code, name in LANGUAGES %}<option value="{{ code }}"{% if code == LANGUAGE_CODE %} selected{% endif %}>{{ name }}</option>{% endfor %}</select></form></div></div></nav>
<nav class="navbar bg-white sticky-top"><div class="container shell"><a class="navbar-brand fw-bold" href="/">my2dos</a><div class="d-flex align-items-center gap-3"><span class="small d-none d-sm-inline"><a class="nav-soft" href="/">{% for label in nav_overview_labels %}{{ label }}{% if not forloop.last %} · {% endif %}{% endfor %}</a>{% for link in nav_hidden_links %}<a class="nav-soft ms-2" href="{{ link.url }}">{{ link.label }}</a>{% endfor %}</span>{% if user.is_authenticated %}<a class="small nav-soft" href="{% url 'journal' %}">{% trans "Journal" %}</a><a class="small nav-soft" href="{% url 'kanban_list' %}">{% trans "Kanban" %}</a><a class="small nav-soft" href="{% url 'team_list' %}">{% trans "Teams" %}</a><div class="dropdown"><button class="btn btn-sm btn-outline-secondary dropdown-toggle user-menu-btn" data-bs-toggle="dropdown">{{ user.get_full_name|default:user.username }}</button><ul class="dropdown-menu dropdown-menu-end"><li><a class="dropdown-item" href="{% url 'settings' %}">{% trans "Settings" %}</a></li><li><button id="pwa-install" type="button" class="dropdown-item d-none">{% trans "Install app" %}</button></li>{% if user.is_staff %}<li><a class="dropdown-item" href="/admin/">Admin</a></li>{% endif %}<li><hr class="dropdown-divider"></li><li><form method="post" action="{% url 'logout' %}">{% csrf_token %}<button class="dropdown-item">{% trans "Logout" %}</button></form></li></ul></div>{% else %}<a class="btn btn-sm btn-dark" href="{% url 'login' %}">{% trans "Login" %}</a>{% endif %}</div></div></nav>
<main class="container shell py-4">{% for message in messages %}<div class="alert alert-{{ message.tags|default:'info' }}">{{ message }}</div>{% endfor %}{% block content %}{% endblock %}</main>
<script>
document.body.addEventListener('htmx:configRequest', e => {e.detail.headers['X-CSRFToken']='{{ csrf_token }}'});
@@ -45,6 +45,7 @@ window.quickComposer = window.quickComposer || function(tags, itemId=null){
window.searchBox = function(tags){return{open:false,active:0,q:'',tags:tags||[],get filtered(){let q=this.q.toLowerCase();return this.tags.filter(t=>('#'+t).toLowerCase().startsWith(q))},onInput(e){this.q=e.target.value;this.open=this.q.startsWith('#')},onKeydown(e){if(!this.open)return;if(e.key==='ArrowDown'){e.preventDefault();this.active=(this.active+1)%Math.max(this.filtered.length,1)}else if(e.key==='ArrowUp'){e.preventDefault();this.active=(this.active-1+Math.max(this.filtered.length,1))%Math.max(this.filtered.length,1)}else if(e.key==='Enter'||e.key==='Tab'){if(this.filtered.length){e.preventDefault();this.select(this.filtered[this.active])}}else if(e.key==='Escape'){this.open=false}},select(tag){let url=new URL(window.location.href);url.searchParams.set('tag',tag);url.searchParams.delete('q');window.location.href=url.pathname+'?'+url.searchParams.toString()}}}
function enhanceCodeBlocks(root=document){root.querySelectorAll('.content pre:not([data-copy])').forEach(pre=>{pre.dataset.copy='1';let btn=document.createElement('button');btn.type='button';btn.className='copy-code btn btn-sm btn-light';btn.title='Kopieren';btn.innerHTML='⧉';btn.addEventListener('click',()=>navigator.clipboard.writeText(pre.querySelector('code')?.innerText || pre.innerText).then(()=>{btn.innerHTML='✓';setTimeout(()=>btn.innerHTML='⧉',1200)}));pre.appendChild(btn)})}
document.addEventListener('DOMContentLoaded',()=>enhanceCodeBlocks());document.body.addEventListener('htmx:afterSwap',e=>enhanceCodeBlocks(e.target));
document.addEventListener('click', event => {let link=event.target.closest('a.tag-filter[data-tag]');if(!link||!(event.ctrlKey||event.metaKey))return;event.preventDefault();let url=new URL(window.location.href);let tag=link.dataset.tag;let tags=url.searchParams.getAll('tag');url.searchParams.delete('tag');if(tags.includes(tag)){tags=tags.filter(t=>t!==tag)}else{tags.push(tag)}tags.forEach(t=>url.searchParams.append('tag',t));window.location.href=url.pathname+(url.searchParams.toString()?'?'+url.searchParams.toString():'')});
let deferredPwaPrompt=null;window.addEventListener('beforeinstallprompt',event=>{event.preventDefault();deferredPwaPrompt=event;document.getElementById('pwa-install')?.classList.remove('d-none')});document.addEventListener('click',async event=>{if(event.target?.id==='pwa-install'&&deferredPwaPrompt){deferredPwaPrompt.prompt();await deferredPwaPrompt.userChoice;deferredPwaPrompt=null;event.target.classList.add('d-none')}});
if('serviceWorker' in navigator){window.addEventListener('load',()=>navigator.serviceWorker.register('/sw.js').catch(()=>{}));}
</script>
+16 -5
View File
@@ -9,17 +9,19 @@
</div>
<div class="row g-4">
<aside class="col-lg-3 order-lg-2">
<div class="card item-card shadow-sm"><div class="card-body">
<div class="filter-panel">
<div class="fw-semibold mb-2">{% trans "Filter" %}</div>
<div class="d-flex flex-wrap gap-2 mb-3">
<a class="btn btn-sm {% if active_kind == 'todo' %}btn-primary{% else %}btn-outline-primary{% endif %}" href="{% if active_kind == 'todo' %}{% query_replace kind=None %}{% else %}{% query_replace kind='todo' %}{% endif %}">Todos{% if active_kind == 'todo' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_kind == 'note' %}btn-secondary{% else %}btn-outline-secondary{% endif %}" href="{% if active_kind == 'note' %}{% query_replace kind=None %}{% else %}{% query_replace kind='note' %}{% endif %}">{% trans "Notes" %}{% if active_kind == 'note' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_kind == 'link' %}btn-success{% else %}btn-outline-success{% endif %}" href="{% if active_kind == 'link' %}{% query_replace kind=None %}{% else %}{% query_replace kind='link' %}{% endif %}">Links{% if active_kind == 'link' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_status == 'open' %}btn-warning{% else %}btn-outline-warning{% endif %}" href="{% if active_status == 'open' %}{% query_replace status=None %}{% else %}{% query_replace status='open' %}{% endif %}">{% trans "Open" %}{% if active_status == 'open' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_status == 'due' %}btn-danger{% else %}btn-outline-danger{% endif %}" href="{% if active_status == 'due' %}{% query_replace status=None %}{% else %}{% query_replace status='due' %}{% endif %}">{% trans "Due" %} <span class="badge {% if active_status == 'due' %}text-bg-light{% else %}text-bg-danger{% endif %}">{{ due_count }}</span>{% if active_status == 'due' %} ×{% endif %}</a>
<a class="btn btn-sm {% if active_status == 'archive' %}btn-dark{% else %}btn-outline-dark{% endif %}" href="{% if active_status == 'archive' %}{% query_replace status=None %}{% else %}{% query_replace status='archive' %}{% endif %}">{% trans "Archive" %}{% if active_status == 'archive' %} ×{% endif %}</a>
</div>
<form class="mb-3 position-relative" method="get" x-data="searchBox([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])" x-on:keydown.window="if(($event.ctrlKey||$event.metaKey)&&$event.key==='f'){ $event.preventDefault(); $refs.search.focus(); $refs.search.select(); }">
{% if active_tag %}<input type="hidden" name="tag" value="{{ active_tag }}">{% endif %}
{% for tag_name in active_tags %}<input type="hidden" name="tag" value="{{ tag_name }}">{% endfor %}
{% if active_tag_mode == 'all' %}<input type="hidden" name="tag_mode" value="all">{% endif %}
{% if active_kind %}<input type="hidden" name="kind" value="{{ active_kind }}">{% endif %}
{% if active_status %}<input type="hidden" name="status" value="{{ active_status }}">{% endif %}
<div class="input-group input-group-sm">
@@ -31,20 +33,29 @@
</div>
</form>
<div class="d-none d-md-block">
<div class="fw-semibold mb-2">{% trans "Tags" %}</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="fw-semibold">{% trans "Tags" %}</div>
{% if active_tags|length > 1 %}
<div class="btn-group btn-group-sm" title="{% trans 'Tag mode' %}">
<a class="btn {% if active_tag_mode == 'any' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode=None %}">{% trans "any" %}</a>
<a class="btn {% if active_tag_mode == 'all' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode='all' %}">{% trans "all" %}</a>
</div>
{% endif %}
</div>
<div hx-get="{% url 'tag_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" hx-trigger="tagsChanged from:body" hx-target="#tag-list" hx-swap="outerHTML">
{% include 'core/_tags.html' %}
</div>
</div>
<details class="d-md-none">
<summary class="btn btn-sm btn-outline-secondary mb-2">{% trans "Show tags" %}</summary>
{% if active_tags|length > 1 %}<div class="btn-group btn-group-sm mb-2"><a class="btn {% if active_tag_mode == 'any' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode=None %}">{% trans "any" %}</a><a class="btn {% if active_tag_mode == 'all' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% query_replace tag_mode='all' %}">{% trans "all" %}</a></div>{% endif %}
<div class="d-flex flex-wrap gap-2">
{% for tag in tags %}
{% if active_tag == tag.name %}<a class="badge rounded-pill text-bg-dark text-decoration-none tag" href="{% query_replace tag=None %}">#{{ tag.name }} ×</a>{% else %}<a class="badge rounded-pill text-bg-light text-decoration-none tag" href="{% query_replace tag=tag.name %}">#{{ tag.name }}</a>{% endif %}
{% if tag.name in active_tags %}<a class="badge rounded-pill text-bg-dark text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" href="{% query_replace tag=None %}">#{{ tag.name }} ×</a>{% else %}<a class="badge rounded-pill text-bg-light text-decoration-none tag tag-filter" data-tag="{{ tag.name }}" href="{% query_replace tag=tag.name %}">#{{ tag.name }}</a>{% endif %}
{% empty %}<span class="text-muted small">{% trans "No tags yet" %}</span>{% endfor %}
</div>
</details>
</div></div>
</div>
</aside>
<section class="col-lg-9">
<form class="desktop-quick-create position-relative mb-4" method="post" enctype="multipart/form-data" hx-post="{{ request.get_full_path }}" hx-target="#items" hx-swap="afterbegin" x-data="quickComposer([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])" x-on:htmx:after-request="if($event.detail.successful){text='';open=false;$el.reset()}">
+1 -1
View File
@@ -23,7 +23,7 @@
</div>
<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>
{% if item.kind == 'todo' %}<div class="col-md-4"><input class="form-control" type="datetime-local" name="due_at" value="{{ item.due_at|date:'Y-m-d\\TH:i' }}"></div>{% endif %}
{% 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>
{% if item.kind == 'link' %}<input class="form-control" type="url" name="url" value="{{ item.url }}" placeholder="URL">{% endif %}
+3 -3
View File
@@ -3,18 +3,18 @@
{% block content %}
<div class="row g-4">
<aside class="col-lg-3 order-lg-2">
<div class="card item-card shadow-sm"><div class="card-body">
<div class="filter-panel">
<div class="d-flex justify-content-between align-items-center mb-2">
<a class="btn btn-sm btn-outline-dark" href="?year={{ prev_year }}&month={{ prev_month }}">←</a>
<strong>{{ month }}/{{ year }}</strong>
<a class="btn btn-sm btn-outline-dark" href="?year={{ next_year }}&month={{ next_month }}">→</a>
</div>
<table class="table table-sm text-center mb-3"><thead><tr><th>Mo</th><th>Di</th><th>Mi</th><th>Do</th><th>Fr</th><th>Sa</th><th>So</th></tr></thead><tbody>
<table class="table table-sm text-center mb-3 small"><thead><tr><th>Mo</th><th>Di</th><th>Mi</th><th>Do</th><th>Fr</th><th>Sa</th><th>So</th></tr></thead><tbody>
{% for week in weeks %}<tr>{% for d in week %}<td>{% if d.day %}{% if d.has_entries %}<a class="btn btn-sm {% if selected_day == d.date %}btn-info{% else %}btn-outline-info{% endif %}" href="?day={{ d.date }}&year={{ year }}&month={{ month }}">{{ d.day }}</a>{% else %}<span class="small text-muted">{{ d.day }}</span>{% endif %}{% endif %}</td>{% endfor %}</tr>{% endfor %}
</tbody></table>
<div class="fw-semibold mb-2">{% trans "Tags" %}</div>
{% include 'core/_tags.html' %}
</div></div>
</div>
</aside>
<section class="col-lg-9">
<h1 class="h4 mb-3">Journal {{ selected_day }}</h1>
+65 -38
View File
@@ -2,60 +2,87 @@
{% load i18n %}
{% block content %}
<a class="btn btn-sm btn-outline-secondary mb-3" href="/">← {% trans "back" %}</a>
<div class="row g-4">
<section class="col-lg-6">
<div class="card item-card shadow-sm"><div class="card-body">
<h1 class="h4 mb-3">{% trans "User settings" %}</h1>
<form method="post" class="vstack gap-3">
<div class="row g-4 align-items-start">
<aside class="col-lg-3">
<div class="card item-card shadow-sm sticky-lg-top" style="top:5rem"><div class="card-body">
<h1 class="h5 mb-3">{% trans "Settings" %}</h1>
<nav class="nav nav-pills flex-column gap-1">
<a class="nav-link" href="#profile">{% trans "Profile" %}</a>
<a class="nav-link" href="#preferences">{% trans "View & language" %}</a>
<a class="nav-link" href="#invites">{% trans "Invitations" %}</a>
<a class="nav-link" href="#api">{% trans "API keys" %}</a>
</nav>
</div></div>
</aside>
<section class="col-lg-9 vstack gap-4">
<div id="profile" class="card item-card shadow-sm"><div class="card-body">
<h2 class="h4 mb-1">{% trans "Profile" %}</h2>
<p class="text-muted small mb-3">{% trans "Manage your personal user data." %}</p>
<form method="post" class="row g-3">
{% csrf_token %}<input type="hidden" name="action" value="profile">
<div><label class="form-label">{% trans "First name" %}</label>{{ profile_form.first_name }}</div>
<div><label class="form-label">{% trans "Last name" %}</label>{{ profile_form.last_name }}</div>
<div><label class="form-label">{% trans "Email" %}</label>{{ profile_form.email }}</div>
<button class="btn btn-dark">{% trans "Save" %}</button>
<div class="col-md-6"><label class="form-label">{% trans "First name" %}</label>{{ profile_form.first_name }}</div>
<div class="col-md-6"><label class="form-label">{% trans "Last name" %}</label>{{ profile_form.last_name }}</div>
<div class="col-12"><label class="form-label">{% trans "Email" %}</label>{{ profile_form.email }}</div>
<div class="col-12"><button class="btn btn-dark">{% trans "Save profile" %}</button></div>
</form>
</div></div>
</section>
<section class="col-lg-6">
<div class="card item-card shadow-sm mb-3"><div class="card-body">
<h2 class="h5 mb-3">{% trans "Default view" %}</h2>
<form method="post" class="vstack gap-2">
<div id="preferences" class="card item-card shadow-sm"><div class="card-body">
<h2 class="h4 mb-1">{% trans "View & language" %}</h2>
<p class="text-muted small mb-3">{% trans "Choose your language and configure the default overview." %}</p>
<form method="post" class="vstack gap-3">
{% csrf_token %}<input type="hidden" name="action" value="preferences">
{% for field in preference_form %}
{% if field.field.widget.input_type == 'checkbox' %}
<label class="form-check"><input class="form-check-input" type="checkbox" name="{{ field.name }}" {% if field.value %}checked{% endif %}> <span class="form-check-label">{{ field.label }}</span></label>
{% else %}
<div><label class="form-label">{{ field.label }}</label>{{ field }}</div>
{% endif %}
{% endfor %}
<button class="btn btn-dark mt-2">{% trans "Save default view" %}</button>
<div class="row g-3">
{% for field in preference_form %}
{% if field.name == 'language' or field.name == 'overview_lines' %}
<div class="col-md-6"><label class="form-label">{{ field.label }}</label>{{ field }}</div>
{% endif %}
{% endfor %}
<div class="col-12">
<div class="form-label mb-2">{% trans "Sorting" %}</div>
<label class="form-check border rounded p-3"><input class="form-check-input" type="checkbox" name="due_first" {% if preference_form.due_first.value %}checked{% endif %}> <span class="form-check-label fw-semibold">{{ preference_form.due_first.label }}</span><div class="small text-muted">{% trans "Overdue and due-today tasks are sorted to the top of the overview." %}</div></label>
</div>
</div>
<div>
<div class="form-label mb-2">{% trans "Show in overview" %}</div>
<div class="row g-2">
{% for field in preference_form %}
{% if field.field.widget.input_type == 'checkbox' and field.name != 'due_first' %}
<div class="col-md-6"><label class="form-check border rounded p-2 ps-4 h-100"><input class="form-check-input" type="checkbox" name="{{ field.name }}" {% if field.value %}checked{% endif %}> <span class="form-check-label">{{ field.label }}</span></label></div>
{% endif %}
{% endfor %}
</div>
</div>
<div><button class="btn btn-dark">{% trans "Save view settings" %}</button></div>
</form>
</div></div>
<div class="card item-card shadow-sm mb-3"><div class="card-body">
<h2 id="teams" class="h5 mb-3">{% trans "Teams" %}</h2>
<form method="post" class="vstack gap-3 mb-3">
{% csrf_token %}<input type="hidden" name="action" value="team">
<div><label class="form-label">{% trans "Team name" %}</label>{{ team_form.name }}</div>
<div><label class="form-label">{% trans "Team tag" %}</label><div class="small text-muted mb-2">{% trans "This tag automatically turns items into team items, e.g. #my2dos." %}</div>{{ team_form.slug }}</div>
<button class="btn btn-dark">{% trans "Create team" %}</button>
<div id="invites" class="card item-card shadow-sm"><div class="card-body">
<h2 class="h4 mb-1">{% trans "User invitations" %}</h2>
<p class="text-muted small mb-3">{% trans "Invite new users during the test phase. Only people with an invitation link can register." %}</p>
<form method="post" class="row g-3 mb-4">
{% csrf_token %}<input type="hidden" name="action" value="userinvite">
<div class="col-md-8"><label class="form-label">{% trans "Email" %}</label>{{ user_invite_form.email }}</div>
<div class="col-md-4 d-flex align-items-end"><button class="btn btn-dark w-100">{% trans "Create invitation link" %}</button></div>
</form>
<div class="vstack gap-2">
{% for team in teams %}<a class="border rounded p-2 text-decoration-none" href="{% url 'team_detail' team.id %}"><strong>{{ team.name }}</strong> <span class="text-muted">#{{ team.slug }}</span></a>{% empty %}<span class="text-muted">{% trans "No teams yet." %}</span>{% endfor %}
{% for invite in user_invites %}<div id="invite-{{ invite.id }}" class="border rounded p-3"><div class="d-flex justify-content-between gap-2"><strong>{{ invite.email }}</strong><form method="post" action="{% url 'delete_user_invite' invite.id %}" hx-post="{% url 'delete_user_invite' invite.id %}" hx-target="#invite-{{ invite.id }}" hx-swap="outerHTML">{% csrf_token %}<button class="btn btn-sm btn-outline-danger">{% trans "Delete" %}</button></form></div><code class="small user-select-all">{{ request.scheme }}://{{ request.get_host }}{% url 'accept_user_invite' invite.token %}</code></div>{% empty %}<span class="text-muted">{% trans "No open invitations." %}</span>{% endfor %}
</div>
</div></div>
<div class="card item-card shadow-sm mb-3"><div class="card-body">
<h2 class="h5 mb-3">{% trans "Create API key" %}</h2>
<form method="post" class="vstack gap-3">
<div id="api" class="card item-card shadow-sm"><div class="card-body">
<h2 class="h4 mb-1">{% trans "API keys" %}</h2>
<p class="text-muted small mb-3">{% trans "Create API keys for integrations and optionally restrict them to tags." %}</p>
<form method="post" class="vstack gap-3 mb-4">
{% csrf_token %}<input type="hidden" name="action" value="apikey">
<div><label class="form-label">{% trans "Name" %}</label>{{ key_form.name }}</div>
<div><label class="form-label">{% trans "Restrict to tags" %}</label><div class="small text-muted mb-2">{% trans "No selection = access to all own items." %}</div>{{ key_form.tags }}</div>
<button class="btn btn-dark">{% trans "Generate API key" %}</button>
<button class="btn btn-dark align-self-start">{% trans "Generate API key" %}</button>
</form>
</div></div>
<div class="card item-card shadow-sm"><div class="card-body">
<h2 class="h5 mb-3">{% trans "API keys" %}</h2>
<div class="vstack gap-2">
{% for key in api_keys %}
<div class="border rounded p-2">
<div class="border rounded p-3">
<div class="d-flex justify-content-between gap-2"><strong>{{ key.name }}</strong><form method="post" action="{% url 'delete_api_key' key.id %}">{% csrf_token %}<button class="btn btn-sm btn-outline-danger">{% trans "Delete" %}</button></form></div>
<code class="small user-select-all">{{ key.token }}</code>
<div class="small text-muted">{% trans "Tags" %}: {% for tag in key.tags.all %}#{{ tag.name }} {% empty %}{% trans "all" %}{% endfor %}</div>
+1 -1
View File
@@ -1,7 +1,7 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% block content %}
<a class="btn btn-sm btn-outline-secondary mb-3" href="{% url 'settings' %}">← {% trans "Settings" %}</a>
<a class="btn btn-sm btn-outline-secondary mb-3" href="{% url 'team_list' %}">← {% trans "Teams" %}</a>
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<h1 class="h3 mb-1">{% trans "Team" %} {{ team.name }}</h1>
+56
View File
@@ -0,0 +1,56 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h1 class="h3 mb-1">{% trans "Teams" %}</h1>
<div class="text-muted">{% trans "All teams you belong to." %}</div>
</div>
<a class="btn btn-sm btn-outline-secondary" href="/">← {% trans "back" %}</a>
</div>
<div class="row g-4">
<section class="col-lg-4 order-lg-2">
<div class="card item-card shadow-sm"><div class="card-body">
<h2 class="h5 mb-3">{% trans "Create team" %}</h2>
<form method="post" class="vstack gap-3">
{% csrf_token %}
<div><label class="form-label">{% trans "Team name" %}</label>{{ team_form.name }}</div>
<div><label class="form-label">{% trans "Team tag" %}</label>{{ team_form.slug }}<div class="small text-muted mt-1">{% trans "This tag automatically turns items into team items, e.g. #my2dos." %}</div></div>
<button class="btn btn-dark">{% trans "Create team" %}</button>
</form>
</div></div>
</section>
<section class="col-lg-8">
<div class="vstack gap-3">
{% for s in stats %}
<article class="card item-card shadow-sm"><div class="card-body">
<div class="d-flex justify-content-between align-items-start gap-3 mb-2">
<div>
<h2 class="h5 mb-1"><a class="text-decoration-none" href="{% url 'team_detail' s.team.id %}">{{ s.team.name }}</a></h2>
<div class="small text-muted">#{{ s.team.slug }} · {{ s.members_count }} {% trans "members" %}</div>
</div>
<a class="btn btn-sm btn-outline-primary" href="{% url 'team_detail' s.team.id %}">{% trans "Open team" %}</a>
</div>
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge text-bg-light">{{ s.items_count }} {% trans "items" %}</span>
<span class="badge text-bg-warning">{{ s.open_todos_count }} {% trans "open tasks" %}</span>
</div>
<div class="vstack gap-2">
{% for item in s.recent_items %}
<a class="border rounded p-2 text-decoration-none text-dark" href="{{ item.get_absolute_url }}">
<span class="badge text-bg-light me-1">{{ item.get_kind_display }}</span>{{ item.content|truncatechars:120 }}
</a>
{% empty %}
<div class="text-muted small">{% trans "No team items yet." %}</div>
{% endfor %}
</div>
</div></article>
{% empty %}
<div class="card item-card shadow-sm"><div class="card-body text-muted">{% trans "You are not a member of any team yet." %}</div></div>
{% endfor %}
</div>
</section>
</div>
{% endblock %}
+4
View File
@@ -6,13 +6,17 @@ urlpatterns = [
path('', views.index, name='index'),
path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('invite/<str:token>/', views.accept_user_invite, name='accept_user_invite'),
path('new/', views.new_item, name='new_item'),
path('journal/', views.journal, name='journal'),
path('kanban/', views.kanban_list, name='kanban_list'),
path('kanban/<int:pk>/', views.kanban_detail, name='kanban_detail'),
path('kanban/<int:pk>/move/<int:item_pk>/', views.kanban_move, name='kanban_move'),
path('language/', views.set_user_language, name='set_user_language'),
path('settings/', views.settings_view, name='settings'),
path('settings/api-keys/<int:pk>/delete/', views.delete_api_key, name='delete_api_key'),
path('settings/invites/<int:pk>/delete/', views.delete_user_invite, name='delete_user_invite'),
path('teams/', views.team_list, name='team_list'),
path('teams/<int:pk>/', views.team_detail, name='team_detail'),
path('teams/invite/<str:token>/', views.team_accept_invite, name='team_accept_invite'),
path('items/<int:pk>/', views.item_detail, name='item_detail'),
+148 -28
View File
@@ -2,21 +2,24 @@ import json
import re
from functools import wraps
from calendar import Calendar
from datetime import date
from datetime import date, datetime, time
from html import unescape
from urllib.request import Request, urlopen
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.db.models import Case, IntegerField, Q, Value, When
from django.http import HttpResponse, JsonResponse
from django.urls import reverse
from django.utils import timezone
from django.utils import translation
from django.utils.translation import gettext as _
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods, require_POST
from .forms import ApiKeyForm, EditItemForm, KanbanForm, QuickItemForm, TeamForm, TeamInviteForm, UserPreferenceForm, UserSettingsForm
from .models import ApiKey, Attachment, Item, Kanban, Tag, Team, TeamInvite, TeamMembership, UserPreference
from .forms import ApiKeyForm, EditItemForm, InviteRegistrationForm, KanbanForm, QuickItemForm, TeamForm, TeamInviteForm, UserInviteForm, UserPreferenceForm, UserSettingsForm
from .models import ApiKey, Attachment, Item, Kanban, Tag, Team, TeamInvite, TeamMembership, UserInvite, UserPreference
COMMAND_RE = re.compile(r'/(todo|note|link|journal|public|private)\b', re.I)
URL_RE = re.compile(r'https?://\S+')
@@ -51,6 +54,19 @@ def parse_quick_content(raw):
return kind, visibility, content, url
def parse_due_at_from_post(post):
due_date = (post.get('due_date') or '').strip()
due_time = (post.get('due_time') or '').strip()
if not due_date:
return None
try:
parsed_date = date.fromisoformat(due_date)
parsed_time = time.fromisoformat(due_time) if due_time else time(23, 59)
return timezone.make_aware(datetime.combine(parsed_date, parsed_time))
except ValueError:
return None
def attach_files_and_replace_tokens(item, files):
replacements = {}
for file in files:
@@ -154,7 +170,9 @@ def index(request):
response['HX-Trigger'] = 'tagsChanged'
return response
return redirect('index')
tag = request.GET.get('tag')
active_tags = [t.strip().lower() for t in request.GET.getlist('tag') if t.strip()]
active_tag_mode = 'all' if request.GET.get('tag_mode') == 'all' else 'any'
tag = active_tags[0] if active_tags else None
kind = request.GET.get('kind')
status = request.GET.get('status')
day = request.GET.get('day')
@@ -165,14 +183,18 @@ def index(request):
query.pop('q', None)
return redirect(f'{request.path}?{query.urlencode()}')
items = visible_items(request.user)
prefs, created = UserPreference.objects.get_or_create(user=request.user)
applied_preferences = False
if tag:
items = items.filter(tags__name=tag)
if active_tags:
if active_tag_mode == 'all':
for tag_name in active_tags:
items = items.filter(tags__name=tag_name)
else:
items = items.filter(tags__name__in=active_tags).distinct()
if kind in dict(Item.Kind.choices):
items = items.filter(kind=kind)
elif not any([tag, status, q, day]):
applied_preferences = True
prefs, _ = UserPreference.objects.get_or_create(user=request.user)
type_filter = Q()
if prefs.show_notes:
type_filter |= Q(kind=Item.Kind.NOTE)
@@ -189,15 +211,24 @@ def index(request):
items = items.filter(created_at__date=day)
if status == 'open':
items = items.filter(kind=Item.Kind.TODO, is_done=False)
elif status == 'due':
today_end = timezone.make_aware(datetime.combine(timezone.localdate(), time(23, 59, 59)))
items = items.filter(kind=Item.Kind.TODO, is_done=False, due_at__lte=today_end).order_by('due_at', '-created_at')
elif status == 'archive':
items = items.filter(kind=Item.Kind.TODO, is_done=True)
elif not applied_preferences:
items = items.exclude(kind=Item.Kind.TODO, is_done=True)
if q:
items = items.filter(Q(content__icontains=q) | Q(url__icontains=q) | Q(tags__name__icontains=q)).distinct()
prefs, _ = UserPreference.objects.get_or_create(user=request.user)
today_end = timezone.make_aware(datetime.combine(timezone.localdate(), time(23, 59, 59)))
due_count = items.filter(kind=Item.Kind.TODO, is_done=False, due_at__lte=today_end).count()
if prefs.due_first and status != 'archive':
items = items.annotate(due_rank=Case(
When(kind=Item.Kind.TODO, is_done=False, due_at__lte=today_end, then=Value(0)),
default=Value(1), output_field=IntegerField(),
)).order_by('due_rank', 'due_at', '-created_at')
return render(request, 'core/index.html', {
'form': QuickItemForm(), 'items': items, 'tags': Tag.objects.all(), 'active_tag': tag, 'active_kind': kind, 'active_status': status, 'active_day': day, 'q': q, 'overview_lines': prefs.overview_lines,
'form': QuickItemForm(), 'items': items, 'tags': Tag.objects.all(), 'active_tag': tag, 'active_tags': active_tags, 'active_tag_mode': active_tag_mode, 'active_kind': kind, 'active_status': status, 'active_day': day, 'q': q, 'due_count': due_count, 'overview_lines': prefs.overview_lines,
})
@@ -254,13 +285,32 @@ def journal(request):
weeks = [[{'day': d, 'date': date(year, month, d).isoformat() if d else '', 'has_entries': d and date(year, month, d) in activity_dates} for d in week] for week in Calendar(firstweekday=0).monthdayscalendar(year, month)]
prev_month, prev_year = (12, year - 1) if month == 1 else (month - 1, year)
next_month, next_year = (1, year + 1) if month == 12 else (month + 1, year)
prefs, _ = UserPreference.objects.get_or_create(user=request.user)
prefs, created = UserPreference.objects.get_or_create(user=request.user)
return render(request, 'core/journal.html', {
'form': QuickItemForm(), 'items': items, 'tags': Tag.objects.all(), 'weeks': weeks, 'year': year, 'month': month, 'selected_day': selected_day,
'prev_year': prev_year, 'prev_month': prev_month, 'next_year': next_year, 'next_month': next_month, 'overview_lines': prefs.overview_lines,
})
@require_POST
def set_user_language(request):
language = request.POST.get('language')
next_url = request.POST.get('next') or '/'
if language not in {'de', 'en', ''}:
language = ''
if request.user.is_authenticated:
prefs, created = UserPreference.objects.get_or_create(user=request.user)
prefs.language = language
prefs.save(update_fields=['language'])
response = redirect(next_url)
if language:
translation.activate(language)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
else:
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
return response
@login_required
def settings_view(request):
if request.method == 'POST':
@@ -271,7 +321,7 @@ def settings_view(request):
messages.success(request, _('Settings saved.'))
return redirect('settings')
elif request.POST.get('action') == 'preferences':
prefs, _ = UserPreference.objects.get_or_create(user=request.user)
prefs, created = UserPreference.objects.get_or_create(user=request.user)
pref_form = UserPreferenceForm(request.POST, instance=prefs)
if pref_form.is_valid():
pref_form.save()
@@ -286,24 +336,22 @@ def settings_view(request):
key_form.save_m2m()
messages.success(request, _('API key created: %(token)s') % {'token': api_key.token})
return redirect('settings')
elif request.POST.get('action') == 'team':
team_form = TeamForm(request.POST)
if team_form.is_valid():
team = team_form.save(commit=False)
team.owner = request.user
team.save()
TeamMembership.objects.create(team=team, user=request.user, role=TeamMembership.Role.OWNER)
Tag.objects.get_or_create(name=team.slug)
messages.success(request, _('Team %(team)s created. Use #%(slug)s for team items.') % {'team': team.name, 'slug': team.slug})
return redirect('team_detail', pk=team.pk)
prefs, _ = UserPreference.objects.get_or_create(user=request.user)
elif request.POST.get('action') == 'userinvite':
invite_form = UserInviteForm(request.POST)
if invite_form.is_valid():
invite = invite_form.save(commit=False)
invite.invited_by = request.user
invite.save()
messages.success(request, _('Invitation link created: %(url)s') % {'url': request.build_absolute_uri(reverse("accept_user_invite", args=[invite.token]))})
return redirect('settings')
prefs, created = UserPreference.objects.get_or_create(user=request.user)
return render(request, 'core/settings.html', {
'profile_form': UserSettingsForm(instance=request.user),
'preference_form': UserPreferenceForm(instance=prefs),
'key_form': ApiKeyForm(),
'team_form': TeamForm(),
'user_invite_form': UserInviteForm(),
'user_invites': request.user.sent_user_invites.filter(accepted_at__isnull=True),
'api_keys': request.user.api_keys.prefetch_related('tags'),
'teams': Team.objects.filter(memberships__user=request.user).distinct(),
})
@@ -315,6 +363,68 @@ def delete_api_key(request, pk):
return redirect('settings')
@login_required
@require_POST
def delete_user_invite(request, pk):
invite = get_object_or_404(UserInvite, pk=pk, invited_by=request.user, accepted_at__isnull=True)
invite.delete()
if request.headers.get('HX-Request'):
return HttpResponse('')
messages.success(request, _('Invitation deleted.'))
return redirect('settings')
def accept_user_invite(request, token):
invite = get_object_or_404(UserInvite, token=token, accepted_at__isnull=True)
if request.user.is_authenticated:
invite.accepted_at = timezone.now()
invite.save(update_fields=['accepted_at'])
messages.success(request, _('Invitation accepted.'))
return redirect('index')
if request.method == 'POST':
form = InviteRegistrationForm(request.POST)
if form.is_valid():
user = form.save(commit=False)
if invite.email and not user.email:
user.email = invite.email
user.save()
invite.accepted_at = timezone.now()
invite.save(update_fields=['accepted_at'])
login(request, user)
messages.success(request, _('Welcome to my2dos.'))
return redirect('index')
else:
form = InviteRegistrationForm(initial={'email': invite.email})
return render(request, 'core/accept_user_invite.html', {'invite': invite, 'form': form})
@login_required
def team_list(request):
if request.method == 'POST':
team_form = TeamForm(request.POST)
if team_form.is_valid():
team = team_form.save(commit=False)
team.owner = request.user
team.save()
TeamMembership.objects.create(team=team, user=request.user, role=TeamMembership.Role.OWNER)
Tag.objects.get_or_create(name=team.slug)
messages.success(request, _('Team %(team)s created. Use #%(slug)s for team items.') % {'team': team.name, 'slug': team.slug})
return redirect('team_detail', pk=team.pk)
teams = Team.objects.filter(memberships__user=request.user).distinct().prefetch_related('memberships__user')
stats = []
base_items = visible_items(request.user)
for team in teams:
team_items = base_items.filter(team=team)
stats.append({
'team': team,
'members_count': team.memberships.count(),
'items_count': team_items.count(),
'open_todos_count': team_items.filter(kind=Item.Kind.TODO, is_done=False).count(),
'recent_items': team_items[:3],
})
return render(request, 'core/team_list.html', {'stats': stats, 'team_form': TeamForm(), 'overview_lines': request.user.preferences.overview_lines if hasattr(request.user, 'preferences') else 4})
@login_required
def team_detail(request, pk):
membership = get_object_or_404(TeamMembership.objects.select_related('team'), team_id=pk, user=request.user)
@@ -384,7 +494,11 @@ def edit_item(request, pk):
if request.method == 'POST':
form = EditItemForm(request.POST, request.FILES, instance=item)
if form.is_valid():
item = form.save()
item = form.save(commit=False)
if item.kind == Item.Kind.TODO:
item.due_at = parse_due_at_from_post(request.POST)
item.save()
form.save_m2m()
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata()
apply_team_from_tags(item, request.user, force_private=item.visibility == Item.Visibility.PRIVATE)
@@ -403,7 +517,11 @@ def item_editor(request, pk):
if request.method == 'POST':
form = EditItemForm(request.POST, request.FILES, instance=item)
if form.is_valid():
item = form.save()
item = form.save(commit=False)
if item.kind == Item.Kind.TODO:
item.due_at = parse_due_at_from_post(request.POST)
item.save()
form.save_m2m()
attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata()
apply_team_from_tags(item, request.user, force_private=item.visibility == Item.Visibility.PRIVATE)
@@ -468,7 +586,9 @@ def kanban_move(request, pk, item_pk):
@login_required
def tag_list(request):
return render(request, 'core/_tags.html', {'tags': Tag.objects.all(), 'active_tag': request.GET.get('tag')})
active_tags = [t.strip().lower() for t in request.GET.getlist('tag') if t.strip()]
active_tag_mode = 'all' if request.GET.get('tag_mode') == 'all' else 'any'
return render(request, 'core/_tags.html', {'tags': Tag.objects.all(), 'active_tag': active_tags[0] if active_tags else None, 'active_tags': active_tags, 'active_tag_mode': active_tag_mode})
@login_required
+339 -190
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-23 14:50+0200\n"
"POT-Creation-Date: 2026-08-23 16:56+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,109 +17,143 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: core/forms.py:28
msgid "Write something … /todo /link /public #tag [[note:1]]"
msgstr "Schreibe etwas … /todo /link /public #tag [[note:1]]"
#: core/context_processors.py:9 core/context_processors.py:16
#: core/templates/core/index.html:16
msgid "Notes"
msgstr "Notizen"
#: core/forms.py:65
msgid "Show notes"
msgstr "Notizen anzeigen"
#: core/context_processors.py:9 core/context_processors.py:15
#| msgid "Task"
msgid "Tasks"
msgstr "Aufgaben"
#: core/forms.py:66
msgid "Show open tasks"
msgstr "Offene Aufgaben anzeigen"
#: core/context_processors.py:9 core/context_processors.py:17
#| msgid "Links:"
msgid "Links"
msgstr "Links"
#: core/forms.py:67
msgid "Show completed tasks"
msgstr "Erledigte Aufgaben anzeigen"
#: core/forms.py:68
msgid "Show links"
msgstr "Links anzeigen"
#: core/forms.py:69
msgid "Show journal"
msgstr "Journal anzeigen"
#: core/forms.py:70
msgid "Lines in overview"
msgstr "Zeilen in der Übersicht"
#: core/forms.py:79 core/templates/core/settings.html:37
msgid "Team name"
msgstr "Teamname"
#: core/forms.py:79 core/templates/core/settings.html:38
msgid "Team tag"
msgstr "Team-Tag"
#: core/forms.py:93 core/templates/core/settings.html:13
msgid "Email"
msgstr "E-Mail"
#: core/forms.py:98
msgid "Tag"
msgstr "Tag"
#: core/forms.py:103
msgid "Kanban name"
msgstr "Name des Kanbans"
#: core/forms.py:117
msgid "API key name"
msgstr "Name des API Keys"
#: core/models.py:75 core/templates/core/index.html:5
#: core/templates/core/index.html:53 core/templates/core/journal.html:25
msgid "Task"
msgstr "Aufgabe"
#: core/models.py:76 core/templates/core/index.html:6
#: core/templates/core/index.html:54 core/templates/core/journal.html:26
msgid "Note"
msgstr "Notiz"
#: core/models.py:77
msgid "Link"
msgstr "Link"
#: core/models.py:78 core/templates/core/base.html:24
#: core/context_processors.py:18 core/models.py:98
#: core/templates/core/base.html:24
msgid "Journal"
msgstr "Journal"
#: core/models.py:81 core/templates/core/_edit_form.html:20
#: core/context_processors.py:25
#| msgid "Overdue"
msgid "Overview"
msgstr "Überfällig"
#: core/forms.py:29
msgid "Write something … /todo /link /public #tag [[note:1]]"
msgstr "Schreibe etwas … /todo /link /public #tag [[note:1]]"
#: core/forms.py:61
msgid "Language"
msgstr "Sprache"
#: core/forms.py:62
msgid "Show notes"
msgstr "Notizen anzeigen"
#: core/forms.py:63
msgid "Show open tasks"
msgstr "Offene Aufgaben anzeigen"
#: core/forms.py:64
msgid "Show completed tasks"
msgstr "Erledigte Aufgaben anzeigen"
#: core/forms.py:65
msgid "Show links"
msgstr "Links anzeigen"
#: core/forms.py:66
msgid "Show journal"
msgstr "Journal anzeigen"
#: core/forms.py:67
msgid "Always show due items first"
msgstr "Fällige immer oben"
#: core/forms.py:68
msgid "Lines in overview"
msgstr "Zeilen in der Übersicht"
#: core/forms.py:77 core/forms.py:85 core/forms.py:116
#: core/templates/core/settings.html:26 core/templates/core/settings.html:66
msgid "Email"
msgstr "E-Mail"
#: core/forms.py:102 core/templates/core/team_list.html:18
msgid "Team name"
msgstr "Teamname"
#: core/forms.py:102 core/templates/core/team_list.html:19
msgid "Team tag"
msgstr "Team-Tag"
#: core/forms.py:121
msgid "Tag"
msgstr "Tag"
#: core/forms.py:126
msgid "Kanban name"
msgstr "Name des Kanbans"
#: core/forms.py:140
msgid "API key name"
msgstr "Name des API Keys"
#: core/models.py:95 core/templates/core/index.html:5
#: core/templates/core/index.html:64 core/templates/core/journal.html:25
msgid "Task"
msgstr "Aufgabe"
#: core/models.py:96 core/templates/core/index.html:6
#: core/templates/core/index.html:65 core/templates/core/journal.html:26
msgid "Note"
msgstr "Notiz"
#: core/models.py:97
msgid "Link"
msgstr "Link"
#: core/models.py:101 core/templates/core/_edit_form.html:20
#: core/templates/core/item_editor.html:25
msgid "Private"
msgstr "Privat"
#: core/models.py:82 core/templates/core/team_detail.html:7
#: core/models.py:102 core/templates/core/team_detail.html:7
msgid "Team"
msgstr "Team"
#: core/models.py:83 core/templates/core/_edit_form.html:21
#: core/models.py:103 core/templates/core/_edit_form.html:21
#: core/templates/core/item_editor.html:25
msgid "Public"
msgstr "Öffentlich"
#: core/models.py:92 core/templates/core/kanban_detail.html:7
#: core/models.py:112 core/templates/core/kanban_detail.html:7
msgid "Inbox"
msgstr "Eingang"
#: core/models.py:94
#: core/models.py:114
msgid "In Progress"
msgstr "In Bearbeitung"
#: core/models.py:95 core/templates/core/kanban_detail.html:10
#: core/models.py:115 core/templates/core/kanban_detail.html:10
#: core/templates/core/kanban_list.html:7
msgid "Done"
msgstr "Erledigt"
#: core/models.py:167
msgid "Browser default"
msgstr "Browser-Standard"
#: core/templates/core/_edit_form.html:5
#: core/templates/core/item_editor.html:11
msgid "Type:"
msgstr "Typ:"
#: core/templates/core/_edit_form.html:7 core/templates/core/index.html:58
#: core/templates/core/_edit_form.html:7 core/templates/core/index.html:69
#: core/templates/core/item_editor.html:13 core/templates/core/journal.html:29
#: core/templates/core/new_item.html:13
msgid "Add tags"
@@ -135,18 +169,23 @@ msgid "Comment, e.g. same task already captured [[12]]"
msgstr "Kommentar, z.B. gleiche Aufgabe bereits erfasst [[12]]"
#: core/templates/core/_edit_form.html:23
msgid "Date/time for todo"
msgstr "Datum/Zeit für Todo"
#: core/templates/core/item_editor.html:26
msgid "Due date"
msgstr "Fälligkeitsdatum"
#: core/templates/core/_edit_form.html:27 core/templates/core/index.html:69
#: core/templates/core/_edit_form.html:23
#: core/templates/core/item_editor.html:26
msgid "Time optional"
msgstr "Uhrzeit optional"
#: core/templates/core/_edit_form.html:27 core/templates/core/index.html:80
msgid "Image from clipboard:"
msgstr "Bild aus Zwischenablage:"
#: core/templates/core/_edit_form.html:29 core/templates/core/index.html:67
#: core/templates/core/_edit_form.html:29 core/templates/core/index.html:78
#: core/templates/core/item_editor.html:9
#: core/templates/core/item_editor.html:31 core/templates/core/journal.html:32
#: core/templates/core/new_item.html:10 core/templates/core/new_item.html:30
#: core/templates/core/settings.html:14
msgid "Save"
msgstr "Speichern"
@@ -168,6 +207,14 @@ msgstr "weniger anzeigen"
msgid "show more"
msgstr "mehr anzeigen"
#: core/templates/core/_item.html:17
msgid "Overdue"
msgstr "Überfällig"
#: core/templates/core/_item.html:17
msgid "Due today"
msgstr "Heute fällig"
#: core/templates/core/_item.html:17
msgid "Due:"
msgstr "Fällig:"
@@ -180,23 +227,30 @@ msgstr "Links:"
msgid "Attachments:"
msgstr "Anhänge:"
#: core/templates/core/_tags.html:9 core/templates/core/index.html:44
#: core/templates/core/_tags.html:9 core/templates/core/index.html:55
msgid "No tags yet"
msgstr "Noch keine Tags"
#: core/templates/core/base.html:24
msgid "Notes · Tasks · Links"
msgstr "Notizen · Aufgaben · Links"
#: core/templates/core/accept_user_invite.html:6
#: core/templates/core/accept_user_invite.html:19
msgid "Create account"
msgstr "Account erstellen"
#: core/templates/core/accept_user_invite.html:7
msgid "You were invited to my2dos. Create your account to continue."
msgstr ""
"Du wurdest zu my2dos eingeladen. Erstelle deinen Account, um fortzufahren."
#: core/templates/core/base.html:24
msgid "Kanban"
msgstr "Kanban"
#: core/templates/core/base.html:24 core/templates/core/settings.html:34
#: core/templates/core/base.html:24 core/templates/core/team_detail.html:4
#: core/templates/core/team_list.html:6
msgid "Teams"
msgstr "Teams"
#: core/templates/core/base.html:24 core/templates/core/team_detail.html:4
#: core/templates/core/base.html:24 core/templates/core/settings.html:8
msgid "Settings"
msgstr "Einstellungen"
@@ -212,57 +266,45 @@ msgstr "Logout"
msgid "Login"
msgstr "Login"
#: core/templates/core/base.html:32 core/templates/core/index.html:99
#: core/templates/core/base.html:32 core/templates/core/index.html:110
#: core/templates/core/new_item.html:9
msgid "Create task"
msgstr "Aufgabe erstellen"
#: core/templates/core/base.html:32 core/templates/core/index.html:100
#, fuzzy
#| msgid "Create link"
#: core/templates/core/base.html:32 core/templates/core/index.html:111
msgid "Save link"
msgstr "Link erstellen"
#: core/templates/core/base.html:32 core/templates/core/index.html:101
#, fuzzy
#| msgid "Create journal entry"
#: core/templates/core/base.html:32 core/templates/core/index.html:112
msgid "Journal entry"
msgstr "Journal-Eintrag erstellen"
#: core/templates/core/base.html:32 core/templates/core/index.html:102
#, fuzzy
#| msgid "Public"
#: core/templates/core/base.html:32 core/templates/core/index.html:113
msgid "public"
msgstr "Öffentlich"
#: core/templates/core/base.html:32 core/templates/core/index.html:103
#, fuzzy
#| msgid "Private"
#: core/templates/core/base.html:32 core/templates/core/index.html:114
msgid "private"
msgstr "Privat"
#: core/templates/core/base.html:32 core/templates/core/index.html:104
#: core/templates/core/base.html:32 core/templates/core/index.html:115
msgid "Insert Markdown code block"
msgstr "Markdown Codeblock einfügen"
#: core/templates/core/base.html:32 core/templates/core/index.html:105
#: core/templates/core/base.html:32 core/templates/core/index.html:116
msgid "Search internal task/note"
msgstr "interne Aufgabe/Notiz suchen"
#: core/templates/core/base.html:32 core/templates/core/index.html:106
#, fuzzy
#| msgid "to insert images into the text."
#: core/templates/core/base.html:32 core/templates/core/index.html:117
msgid "Insert image from attachments"
msgstr "können Bilder im Text eingefügt werden."
#: core/templates/core/base.html:33 core/templates/core/index.html:113
#, fuzzy
#| msgid "Image preview:"
#: core/templates/core/base.html:33 core/templates/core/index.html:124
msgid "Image:"
msgstr "Bildvorschau:"
#: core/templates/core/detail.html:4 core/templates/core/kanban_list.html:4
#: core/templates/core/settings.html:4
#: core/templates/core/settings.html:4 core/templates/core/team_list.html:9
msgid "back"
msgstr "zurück"
@@ -270,50 +312,63 @@ msgstr "zurück"
msgid "Filter"
msgstr "Filter"
#: core/templates/core/index.html:16
msgid "Notes"
msgstr "Notizen"
#: core/templates/core/index.html:18 core/templates/core/kanban_list.html:7
msgid "Open"
msgstr "Offen"
#: core/templates/core/index.html:19
msgid "Due"
msgstr "Fällig:"
#: core/templates/core/index.html:20
msgid "Archive"
msgstr "Archiv"
#: core/templates/core/index.html:26
#: core/templates/core/index.html:28
msgid "Search (Ctrl+F, # for tags)"
msgstr "Suche (Ctrl+F, # für Tags)"
#: core/templates/core/index.html:27
#: core/templates/core/index.html:29
msgid "Clear search"
msgstr "Suche löschen"
#: core/templates/core/index.html:34 core/templates/core/journal.html:15
#: core/templates/core/settings.html:61
#: core/templates/core/index.html:37 core/templates/core/journal.html:15
#: core/templates/core/settings.html:88
msgid "Tags"
msgstr "Tags"
#: core/templates/core/index.html:40
#: core/templates/core/index.html:39
msgid "Tag mode"
msgstr "Tag-Modus"
#: core/templates/core/index.html:40 core/templates/core/index.html:51
msgid "any"
msgstr "beliebig"
#: core/templates/core/index.html:41 core/templates/core/index.html:51
#: core/templates/core/settings.html:88
msgid "all"
msgstr "alle"
#: core/templates/core/index.html:50
msgid "Show tags"
msgstr "Tags anzeigen"
#: core/templates/core/index.html:65 core/templates/core/journal.html:32
#: core/templates/core/index.html:76 core/templates/core/journal.html:32
#: core/templates/core/new_item.html:26
msgid "Gallery"
msgstr "Galerie"
#: core/templates/core/index.html:65 core/templates/core/journal.html:32
#: core/templates/core/index.html:76 core/templates/core/journal.html:32
#: core/templates/core/new_item.html:26
msgid "Photo"
msgstr "Foto"
#: core/templates/core/index.html:66
#: core/templates/core/index.html:77
msgid "Editor"
msgstr "Editor"
#: core/templates/core/index.html:72
#: core/templates/core/index.html:83
msgid "Nothing here yet."
msgstr "Noch nichts vorhanden."
@@ -382,71 +437,104 @@ msgstr "Mit"
msgid "to insert images into the text."
msgstr "können Bilder im Text eingefügt werden."
#: core/templates/core/settings.html:8
msgid "User settings"
msgstr "Benutzereinstellungen"
#: core/templates/core/settings.html:10 core/templates/core/settings.html:20
msgid "Profile"
msgstr "Benutzer"
#: core/templates/core/settings.html:11
msgid "First name"
msgstr "Vorname"
#: core/templates/core/settings.html:11 core/templates/core/settings.html:32
msgid "View & language"
msgstr "Ansicht & Sprache"
#: core/templates/core/settings.html:12
msgid "Last name"
msgstr "Name"
msgid "Invitations"
msgstr "Offene Einladungen:"
#: core/templates/core/settings.html:20
msgid "Default view"
msgstr "Standardansicht"
#: core/templates/core/settings.html:30
msgid "Save default view"
msgstr "Standardansicht speichern"
#: core/templates/core/settings.html:38
msgid "This tag automatically turns items into team items, e.g. #my2dos."
msgstr "Dieser Tag macht Einträge automatisch zu Team-Einträgen, z.B. #my2dos."
#: core/templates/core/settings.html:39
msgid "Create team"
msgstr "Team erstellen"
#: core/templates/core/settings.html:42
msgid "No teams yet."
msgstr "Noch keine Teams."
#: core/templates/core/settings.html:46
msgid "Create API key"
msgstr "API Key erzeugen"
#: core/templates/core/settings.html:49
msgid "Name"
msgstr "Name"
#: core/templates/core/settings.html:50
msgid "Restrict to tags"
msgstr "Auf Tags beschränken"
#: core/templates/core/settings.html:50
msgid "No selection = access to all own items."
msgstr "Keine Auswahl = Zugriff auf alle eigenen Einträge."
#: core/templates/core/settings.html:51
msgid "Generate API key"
msgstr "API Key generieren"
#: core/templates/core/settings.html:55
#: core/templates/core/settings.html:13 core/templates/core/settings.html:75
msgid "API keys"
msgstr "API Keys"
#: core/templates/core/settings.html:59
#: core/templates/core/settings.html:21
msgid "Manage your personal user data."
msgstr "Verwalte deine persönlichen Benutzerdaten."
#: core/templates/core/settings.html:24
msgid "First name"
msgstr "Vorname"
#: core/templates/core/settings.html:25
msgid "Last name"
msgstr "Name"
#: core/templates/core/settings.html:27
msgid "Save profile"
msgstr "Benutzer speichern"
#: core/templates/core/settings.html:33
msgid "Choose your language and configure the default overview."
msgstr "Wähle deine Sprache und konfiguriere die Standardübersicht."
#: core/templates/core/settings.html:43
msgid "Sorting"
msgstr "Einstellungen"
#: core/templates/core/settings.html:44
msgid "Overdue and due-today tasks are sorted to the top of the overview."
msgstr ""
"Überfällige und heute fällige Aufgaben werden in der Übersicht nach oben "
"sortiert."
#: core/templates/core/settings.html:48
msgid "Show in overview"
msgstr "Zeilen in der Übersicht"
#: core/templates/core/settings.html:57
msgid "Save view settings"
msgstr "Ansicht speichern"
#: core/templates/core/settings.html:62
msgid "User invitations"
msgstr "Offene Einladungen:"
#: core/templates/core/settings.html:63
msgid ""
"Invite new users during the test phase. Only people with an invitation link "
"can register."
msgstr ""
#: core/templates/core/settings.html:67 core/templates/core/team_detail.html:26
msgid "Create invitation link"
msgstr "Einladungslink erzeugen"
#: core/templates/core/settings.html:70 core/templates/core/settings.html:86
msgid "Delete"
msgstr "Löschen"
#: core/templates/core/settings.html:61
msgid "all"
msgstr "alle"
#: core/templates/core/settings.html:70 core/templates/core/team_detail.html:29
msgid "No open invitations."
msgstr "Keine offenen Einladungen."
#: core/templates/core/settings.html:63
#: core/templates/core/settings.html:76
msgid "Create API keys for integrations and optionally restrict them to tags."
msgstr ""
"Erzeuge API Keys für Integrationen und beschränke sie optional auf Tags."
#: core/templates/core/settings.html:79
msgid "Name"
msgstr "Name"
#: core/templates/core/settings.html:80
msgid "Restrict to tags"
msgstr "Auf Tags beschränken"
#: core/templates/core/settings.html:80
msgid "No selection = access to all own items."
msgstr "Keine Auswahl = Zugriff auf alle eigenen Einträge."
#: core/templates/core/settings.html:81
msgid "Generate API key"
msgstr "API Key generieren"
#: core/templates/core/settings.html:90
msgid "No API keys yet."
msgstr "Noch keine API Keys."
@@ -467,18 +555,10 @@ msgstr "Mitglieder"
msgid "Invite"
msgstr "Einladen"
#: core/templates/core/team_detail.html:26
msgid "Create invitation link"
msgstr "Einladungslink erzeugen"
#: core/templates/core/team_detail.html:28
msgid "Open invitations:"
msgstr "Offene Einladungen:"
#: core/templates/core/team_detail.html:29
msgid "No open invitations."
msgstr "Keine offenen Einladungen."
#: core/templates/core/team_detail.html:34
msgid "Team items"
msgstr "Team-Einträge"
@@ -488,30 +568,99 @@ msgstr "Team-Einträge"
msgid "No team items yet. Create something with #%(slug)s."
msgstr "Noch keine Team-Einträge. Erstelle etwas mit #%(slug)s."
#: core/views.py:271
#: core/templates/core/team_list.html:7
msgid "All teams you belong to."
msgstr "Alle Teams, zu denen du gehörst."
#: core/templates/core/team_list.html:15 core/templates/core/team_list.html:20
msgid "Create team"
msgstr "Team erstellen"
#: core/templates/core/team_list.html:19
msgid "This tag automatically turns items into team items, e.g. #my2dos."
msgstr "Dieser Tag macht Einträge automatisch zu Team-Einträgen, z.B. #my2dos."
#: core/templates/core/team_list.html:32
msgid "members"
msgstr "Mitglieder"
#: core/templates/core/team_list.html:34
msgid "Open team"
msgstr "Offen"
#: core/templates/core/team_list.html:37
msgid "items"
msgstr "Team-Einträge"
#: core/templates/core/team_list.html:38
msgid "open tasks"
msgstr "Offene Aufgaben anzeigen"
#: core/templates/core/team_list.html:46
msgid "No team items yet."
msgstr "Noch keine Teams."
#: core/templates/core/team_list.html:51
msgid "You are not a member of any team yet."
msgstr "Du bist jetzt Mitglied im Team %(team)s."
#: core/views.py:321
msgid "Settings saved."
msgstr "Einstellungen gespeichert."
#: core/views.py:278
#: core/views.py:328
msgid "Default view saved."
msgstr "Standardansicht gespeichert."
#: core/views.py:287
#: core/views.py:337
#, python-format
msgid "API key created: %(token)s"
msgstr "API Key erzeugt: %(token)s"
#: core/views.py:297
#, python-format
msgid "Team %(team)s created. Use #%(slug)s for team items."
msgstr "Team %(team)s erstellt. Nutze #%(slug)s für Team-Einträge."
#: core/views.py:329
#: core/views.py:345 core/views.py:439
#, python-format
msgid "Invitation link created: %(url)s"
msgstr "Einladungslink erzeugt: %(url)s"
#: core/views.py:349
#: core/views.py:373
msgid "Invitation deleted."
msgstr "Einladungslink erzeugt: %(url)s"
#: core/views.py:382
msgid "Invitation accepted."
msgstr "Einladungslink erzeugt: %(url)s"
#: core/views.py:394
msgid "Welcome to my2dos."
msgstr "Willkommen bei my2dos."
#: core/views.py:411
#, python-format
msgid "Team %(team)s created. Use #%(slug)s for team items."
msgstr "Team %(team)s erstellt. Nutze #%(slug)s für Team-Einträge."
#: core/views.py:459
#, python-format
msgid "You are now a member of team %(team)s."
msgstr "Du bist jetzt Mitglied im Team %(team)s."
#~ msgid "Notes · Tasks · Links"
#~ msgstr "Notizen · Aufgaben · Links"
#~ msgid "Create teams and use their tag to share items."
#~ msgstr "Erstelle Teams und nutze deren Tag, um Einträge zu teilen."
#~ msgid "No teams yet."
#~ msgstr "Noch keine Teams."
#~ msgid "Date/time for todo"
#~ msgstr "Datum/Zeit für Todo"
#~ msgid "Default view"
#~ msgstr "Standardansicht"
#~ msgid "Save default view"
#~ msgstr "Standardansicht speichern"
#~ msgid "Create API key"
#~ msgstr "API Key erzeugen"
+302 -169
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-23 14:50+0200\n"
"POT-Creation-Date: 2026-08-23 16:56+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,109 +18,140 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: core/forms.py:28
msgid "Write something … /todo /link /public #tag [[note:1]]"
#: core/context_processors.py:9 core/context_processors.py:16
#: core/templates/core/index.html:16
msgid "Notes"
msgstr ""
#: core/forms.py:65
msgid "Show notes"
#: core/context_processors.py:9 core/context_processors.py:15
msgid "Tasks"
msgstr ""
#: core/forms.py:66
msgid "Show open tasks"
#: core/context_processors.py:9 core/context_processors.py:17
msgid "Links"
msgstr ""
#: core/forms.py:67
msgid "Show completed tasks"
msgstr ""
#: core/forms.py:68
msgid "Show links"
msgstr ""
#: core/forms.py:69
msgid "Show journal"
msgstr ""
#: core/forms.py:70
msgid "Lines in overview"
msgstr ""
#: core/forms.py:79 core/templates/core/settings.html:37
msgid "Team name"
msgstr ""
#: core/forms.py:79 core/templates/core/settings.html:38
msgid "Team tag"
msgstr ""
#: core/forms.py:93 core/templates/core/settings.html:13
msgid "Email"
msgstr ""
#: core/forms.py:98
msgid "Tag"
msgstr ""
#: core/forms.py:103
msgid "Kanban name"
msgstr ""
#: core/forms.py:117
msgid "API key name"
msgstr ""
#: core/models.py:75 core/templates/core/index.html:5
#: core/templates/core/index.html:53 core/templates/core/journal.html:25
msgid "Task"
msgstr ""
#: core/models.py:76 core/templates/core/index.html:6
#: core/templates/core/index.html:54 core/templates/core/journal.html:26
msgid "Note"
msgstr ""
#: core/models.py:77
msgid "Link"
msgstr ""
#: core/models.py:78 core/templates/core/base.html:24
#: core/context_processors.py:18 core/models.py:98
#: core/templates/core/base.html:24
msgid "Journal"
msgstr ""
#: core/models.py:81 core/templates/core/_edit_form.html:20
#: core/context_processors.py:25
msgid "Overview"
msgstr ""
#: core/forms.py:29
msgid "Write something … /todo /link /public #tag [[note:1]]"
msgstr ""
#: core/forms.py:61
msgid "Language"
msgstr ""
#: core/forms.py:62
msgid "Show notes"
msgstr ""
#: core/forms.py:63
msgid "Show open tasks"
msgstr ""
#: core/forms.py:64
msgid "Show completed tasks"
msgstr ""
#: core/forms.py:65
msgid "Show links"
msgstr ""
#: core/forms.py:66
msgid "Show journal"
msgstr ""
#: core/forms.py:67
msgid "Always show due items first"
msgstr ""
#: core/forms.py:68
msgid "Lines in overview"
msgstr ""
#: core/forms.py:77 core/forms.py:85 core/forms.py:116
#: core/templates/core/settings.html:26 core/templates/core/settings.html:66
msgid "Email"
msgstr ""
#: core/forms.py:102 core/templates/core/team_list.html:18
msgid "Team name"
msgstr ""
#: core/forms.py:102 core/templates/core/team_list.html:19
msgid "Team tag"
msgstr ""
#: core/forms.py:121
msgid "Tag"
msgstr ""
#: core/forms.py:126
msgid "Kanban name"
msgstr ""
#: core/forms.py:140
msgid "API key name"
msgstr ""
#: core/models.py:95 core/templates/core/index.html:5
#: core/templates/core/index.html:64 core/templates/core/journal.html:25
msgid "Task"
msgstr ""
#: core/models.py:96 core/templates/core/index.html:6
#: core/templates/core/index.html:65 core/templates/core/journal.html:26
msgid "Note"
msgstr ""
#: core/models.py:97
msgid "Link"
msgstr ""
#: core/models.py:101 core/templates/core/_edit_form.html:20
#: core/templates/core/item_editor.html:25
msgid "Private"
msgstr ""
#: core/models.py:82 core/templates/core/team_detail.html:7
#: core/models.py:102 core/templates/core/team_detail.html:7
msgid "Team"
msgstr ""
#: core/models.py:83 core/templates/core/_edit_form.html:21
#: core/models.py:103 core/templates/core/_edit_form.html:21
#: core/templates/core/item_editor.html:25
msgid "Public"
msgstr ""
#: core/models.py:92 core/templates/core/kanban_detail.html:7
#: core/models.py:112 core/templates/core/kanban_detail.html:7
msgid "Inbox"
msgstr ""
#: core/models.py:94
#: core/models.py:114
msgid "In Progress"
msgstr ""
#: core/models.py:95 core/templates/core/kanban_detail.html:10
#: core/models.py:115 core/templates/core/kanban_detail.html:10
#: core/templates/core/kanban_list.html:7
msgid "Done"
msgstr ""
#: core/models.py:167
msgid "Browser default"
msgstr ""
#: core/templates/core/_edit_form.html:5
#: core/templates/core/item_editor.html:11
msgid "Type:"
msgstr ""
#: core/templates/core/_edit_form.html:7 core/templates/core/index.html:58
#: core/templates/core/_edit_form.html:7 core/templates/core/index.html:69
#: core/templates/core/item_editor.html:13 core/templates/core/journal.html:29
#: core/templates/core/new_item.html:13
msgid "Add tags"
@@ -136,18 +167,23 @@ msgid "Comment, e.g. same task already captured [[12]]"
msgstr ""
#: core/templates/core/_edit_form.html:23
msgid "Date/time for todo"
#: core/templates/core/item_editor.html:26
msgid "Due date"
msgstr ""
#: core/templates/core/_edit_form.html:27 core/templates/core/index.html:69
#: core/templates/core/_edit_form.html:23
#: core/templates/core/item_editor.html:26
msgid "Time optional"
msgstr ""
#: core/templates/core/_edit_form.html:27 core/templates/core/index.html:80
msgid "Image from clipboard:"
msgstr ""
#: core/templates/core/_edit_form.html:29 core/templates/core/index.html:67
#: core/templates/core/_edit_form.html:29 core/templates/core/index.html:78
#: core/templates/core/item_editor.html:9
#: core/templates/core/item_editor.html:31 core/templates/core/journal.html:32
#: core/templates/core/new_item.html:10 core/templates/core/new_item.html:30
#: core/templates/core/settings.html:14
msgid "Save"
msgstr ""
@@ -169,6 +205,14 @@ msgstr ""
msgid "show more"
msgstr ""
#: core/templates/core/_item.html:17
msgid "Overdue"
msgstr ""
#: core/templates/core/_item.html:17
msgid "Due today"
msgstr ""
#: core/templates/core/_item.html:17
msgid "Due:"
msgstr ""
@@ -181,23 +225,29 @@ msgstr ""
msgid "Attachments:"
msgstr ""
#: core/templates/core/_tags.html:9 core/templates/core/index.html:44
#: core/templates/core/_tags.html:9 core/templates/core/index.html:55
msgid "No tags yet"
msgstr ""
#: core/templates/core/base.html:24
msgid "Notes · Tasks · Links"
#: core/templates/core/accept_user_invite.html:6
#: core/templates/core/accept_user_invite.html:19
msgid "Create account"
msgstr ""
#: core/templates/core/accept_user_invite.html:7
msgid "You were invited to my2dos. Create your account to continue."
msgstr ""
#: core/templates/core/base.html:24
msgid "Kanban"
msgstr ""
#: core/templates/core/base.html:24 core/templates/core/settings.html:34
#: core/templates/core/base.html:24 core/templates/core/team_detail.html:4
#: core/templates/core/team_list.html:6
msgid "Teams"
msgstr ""
#: core/templates/core/base.html:24 core/templates/core/team_detail.html:4
#: core/templates/core/base.html:24 core/templates/core/settings.html:8
msgid "Settings"
msgstr ""
@@ -213,45 +263,45 @@ msgstr ""
msgid "Login"
msgstr ""
#: core/templates/core/base.html:32 core/templates/core/index.html:99
#: core/templates/core/base.html:32 core/templates/core/index.html:110
#: core/templates/core/new_item.html:9
msgid "Create task"
msgstr ""
#: core/templates/core/base.html:32 core/templates/core/index.html:100
#: core/templates/core/base.html:32 core/templates/core/index.html:111
msgid "Save link"
msgstr ""
#: core/templates/core/base.html:32 core/templates/core/index.html:101
#: core/templates/core/base.html:32 core/templates/core/index.html:112
msgid "Journal entry"
msgstr ""
#: core/templates/core/base.html:32 core/templates/core/index.html:102
#: core/templates/core/base.html:32 core/templates/core/index.html:113
msgid "public"
msgstr ""
#: core/templates/core/base.html:32 core/templates/core/index.html:103
#: core/templates/core/base.html:32 core/templates/core/index.html:114
msgid "private"
msgstr ""
#: core/templates/core/base.html:32 core/templates/core/index.html:104
#: core/templates/core/base.html:32 core/templates/core/index.html:115
msgid "Insert Markdown code block"
msgstr ""
#: core/templates/core/base.html:32 core/templates/core/index.html:105
#: core/templates/core/base.html:32 core/templates/core/index.html:116
msgid "Search internal task/note"
msgstr ""
#: core/templates/core/base.html:32 core/templates/core/index.html:106
#: core/templates/core/base.html:32 core/templates/core/index.html:117
msgid "Insert image from attachments"
msgstr ""
#: core/templates/core/base.html:33 core/templates/core/index.html:113
#: core/templates/core/base.html:33 core/templates/core/index.html:124
msgid "Image:"
msgstr ""
#: core/templates/core/detail.html:4 core/templates/core/kanban_list.html:4
#: core/templates/core/settings.html:4
#: core/templates/core/settings.html:4 core/templates/core/team_list.html:9
msgid "back"
msgstr ""
@@ -259,50 +309,63 @@ msgstr ""
msgid "Filter"
msgstr ""
#: core/templates/core/index.html:16
msgid "Notes"
msgstr ""
#: core/templates/core/index.html:18 core/templates/core/kanban_list.html:7
msgid "Open"
msgstr ""
#: core/templates/core/index.html:19
msgid "Due"
msgstr ""
#: core/templates/core/index.html:20
msgid "Archive"
msgstr ""
#: core/templates/core/index.html:26
#: core/templates/core/index.html:28
msgid "Search (Ctrl+F, # for tags)"
msgstr ""
#: core/templates/core/index.html:27
#: core/templates/core/index.html:29
msgid "Clear search"
msgstr ""
#: core/templates/core/index.html:34 core/templates/core/journal.html:15
#: core/templates/core/settings.html:61
#: core/templates/core/index.html:37 core/templates/core/journal.html:15
#: core/templates/core/settings.html:88
msgid "Tags"
msgstr ""
#: core/templates/core/index.html:40
#: core/templates/core/index.html:39
msgid "Tag mode"
msgstr ""
#: core/templates/core/index.html:40 core/templates/core/index.html:51
msgid "any"
msgstr ""
#: core/templates/core/index.html:41 core/templates/core/index.html:51
#: core/templates/core/settings.html:88
msgid "all"
msgstr ""
#: core/templates/core/index.html:50
msgid "Show tags"
msgstr ""
#: core/templates/core/index.html:65 core/templates/core/journal.html:32
#: core/templates/core/index.html:76 core/templates/core/journal.html:32
#: core/templates/core/new_item.html:26
msgid "Gallery"
msgstr ""
#: core/templates/core/index.html:65 core/templates/core/journal.html:32
#: core/templates/core/index.html:76 core/templates/core/journal.html:32
#: core/templates/core/new_item.html:26
msgid "Photo"
msgstr ""
#: core/templates/core/index.html:66
#: core/templates/core/index.html:77
msgid "Editor"
msgstr ""
#: core/templates/core/index.html:72
#: core/templates/core/index.html:83
msgid "Nothing here yet."
msgstr ""
@@ -371,71 +434,101 @@ msgstr ""
msgid "to insert images into the text."
msgstr ""
#: core/templates/core/settings.html:8
msgid "User settings"
#: core/templates/core/settings.html:10 core/templates/core/settings.html:20
msgid "Profile"
msgstr ""
#: core/templates/core/settings.html:11
msgid "First name"
#: core/templates/core/settings.html:11 core/templates/core/settings.html:32
msgid "View & language"
msgstr ""
#: core/templates/core/settings.html:12
msgid "Last name"
msgid "Invitations"
msgstr ""
#: core/templates/core/settings.html:20
msgid "Default view"
msgstr ""
#: core/templates/core/settings.html:30
msgid "Save default view"
msgstr ""
#: core/templates/core/settings.html:38
msgid "This tag automatically turns items into team items, e.g. #my2dos."
msgstr ""
#: core/templates/core/settings.html:39
msgid "Create team"
msgstr ""
#: core/templates/core/settings.html:42
msgid "No teams yet."
msgstr ""
#: core/templates/core/settings.html:46
msgid "Create API key"
msgstr ""
#: core/templates/core/settings.html:49
msgid "Name"
msgstr ""
#: core/templates/core/settings.html:50
msgid "Restrict to tags"
msgstr ""
#: core/templates/core/settings.html:50
msgid "No selection = access to all own items."
msgstr ""
#: core/templates/core/settings.html:51
msgid "Generate API key"
msgstr ""
#: core/templates/core/settings.html:55
#: core/templates/core/settings.html:13 core/templates/core/settings.html:75
msgid "API keys"
msgstr ""
#: core/templates/core/settings.html:59
msgid "Delete"
#: core/templates/core/settings.html:21
msgid "Manage your personal user data."
msgstr ""
#: core/templates/core/settings.html:61
msgid "all"
#: core/templates/core/settings.html:24
msgid "First name"
msgstr ""
#: core/templates/core/settings.html:25
msgid "Last name"
msgstr ""
#: core/templates/core/settings.html:27
msgid "Save profile"
msgstr ""
#: core/templates/core/settings.html:33
msgid "Choose your language and configure the default overview."
msgstr ""
#: core/templates/core/settings.html:43
msgid "Sorting"
msgstr ""
#: core/templates/core/settings.html:44
msgid "Overdue and due-today tasks are sorted to the top of the overview."
msgstr ""
#: core/templates/core/settings.html:48
msgid "Show in overview"
msgstr ""
#: core/templates/core/settings.html:57
msgid "Save view settings"
msgstr ""
#: core/templates/core/settings.html:62
msgid "User invitations"
msgstr ""
#: core/templates/core/settings.html:63
msgid ""
"Invite new users during the test phase. Only people with an invitation link "
"can register."
msgstr ""
#: core/templates/core/settings.html:67 core/templates/core/team_detail.html:26
msgid "Create invitation link"
msgstr ""
#: core/templates/core/settings.html:70 core/templates/core/settings.html:86
msgid "Delete"
msgstr ""
#: core/templates/core/settings.html:70 core/templates/core/team_detail.html:29
msgid "No open invitations."
msgstr ""
#: core/templates/core/settings.html:76
msgid "Create API keys for integrations and optionally restrict them to tags."
msgstr ""
#: core/templates/core/settings.html:79
msgid "Name"
msgstr ""
#: core/templates/core/settings.html:80
msgid "Restrict to tags"
msgstr ""
#: core/templates/core/settings.html:80
msgid "No selection = access to all own items."
msgstr ""
#: core/templates/core/settings.html:81
msgid "Generate API key"
msgstr ""
#: core/templates/core/settings.html:90
msgid "No API keys yet."
msgstr ""
@@ -456,18 +549,10 @@ msgstr ""
msgid "Invite"
msgstr ""
#: core/templates/core/team_detail.html:26
msgid "Create invitation link"
msgstr ""
#: core/templates/core/team_detail.html:28
msgid "Open invitations:"
msgstr ""
#: core/templates/core/team_detail.html:29
msgid "No open invitations."
msgstr ""
#: core/templates/core/team_detail.html:34
msgid "Team items"
msgstr ""
@@ -477,30 +562,78 @@ msgstr ""
msgid "No team items yet. Create something with #%(slug)s."
msgstr ""
#: core/views.py:271
#: core/templates/core/team_list.html:7
msgid "All teams you belong to."
msgstr ""
#: core/templates/core/team_list.html:15 core/templates/core/team_list.html:20
msgid "Create team"
msgstr ""
#: core/templates/core/team_list.html:19
msgid "This tag automatically turns items into team items, e.g. #my2dos."
msgstr ""
#: core/templates/core/team_list.html:32
msgid "members"
msgstr ""
#: core/templates/core/team_list.html:34
msgid "Open team"
msgstr ""
#: core/templates/core/team_list.html:37
msgid "items"
msgstr ""
#: core/templates/core/team_list.html:38
msgid "open tasks"
msgstr ""
#: core/templates/core/team_list.html:46
msgid "No team items yet."
msgstr ""
#: core/templates/core/team_list.html:51
msgid "You are not a member of any team yet."
msgstr ""
#: core/views.py:321
msgid "Settings saved."
msgstr ""
#: core/views.py:278
#: core/views.py:328
msgid "Default view saved."
msgstr ""
#: core/views.py:287
#: core/views.py:337
#, python-format
msgid "API key created: %(token)s"
msgstr ""
#: core/views.py:297
#, python-format
msgid "Team %(team)s created. Use #%(slug)s for team items."
msgstr ""
#: core/views.py:329
#: core/views.py:345 core/views.py:439
#, python-format
msgid "Invitation link created: %(url)s"
msgstr ""
#: core/views.py:349
#: core/views.py:373
msgid "Invitation deleted."
msgstr ""
#: core/views.py:382
msgid "Invitation accepted."
msgstr ""
#: core/views.py:394
msgid "Welcome to my2dos."
msgstr ""
#: core/views.py:411
#, python-format
msgid "Team %(team)s created. Use #%(slug)s for team items."
msgstr ""
#: core/views.py:459
#, python-format
msgid "You are now a member of team %(team)s."
msgstr ""
+2
View File
@@ -28,6 +28,7 @@ MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'core.middleware.UserLanguageMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
@@ -41,6 +42,7 @@ TEMPLATES = [{
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'core.context_processors.overview_navigation',
]},
}]
WSGI_APPLICATION = 'my2dos.wsgi.application'