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 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): class AttachmentInline(admin.TabularInline):
@@ -22,6 +22,7 @@ class ApiKeyAdmin(admin.ModelAdmin):
admin.site.register(Tag) admin.site.register(Tag)
admin.site.register(UserInvite)
admin.site.register(Team) admin.site.register(Team)
admin.site.register(TeamMembership) admin.site.register(TeamMembership)
admin.site.register(TeamInvite) 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 import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _ 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): class MultipleFileInput(forms.ClearableFileInput):
@@ -38,17 +39,12 @@ class EditItemForm(forms.ModelForm):
class Meta: class Meta:
model = Item model = Item
fields = ['content', 'comment', 'visibility', 'due_at', 'url'] fields = ['content', 'comment', 'visibility', 'url']
widgets = { widgets = {
'comment': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), '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'}), '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 UserSettingsForm(forms.ModelForm):
class Meta: class Meta:
@@ -60,16 +56,42 @@ class UserSettingsForm(forms.ModelForm):
class UserPreferenceForm(forms.ModelForm): class UserPreferenceForm(forms.ModelForm):
class Meta: class Meta:
model = UserPreference 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 = { labels = {
'language': _('Language'),
'show_notes': _('Show notes'), 'show_notes': _('Show notes'),
'show_open_todos': _('Show open tasks'), 'show_open_todos': _('Show open tasks'),
'show_done_todos': _('Show completed tasks'), 'show_done_todos': _('Show completed tasks'),
'show_links': _('Show links'), 'show_links': _('Show links'),
'show_journal': _('Show journal'), 'due_first': _('Always show due items first'),
'overview_lines': _('Lines in overview'), '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): 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.conf import settings
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
TAG_RE = re.compile(r'(?<!\w)#([\wäöüÄÖÜß-]+)') TAG_RE = re.compile(r'(?<!\w)#([\wäöüÄÖÜß-]+)')
@@ -19,6 +20,25 @@ class Tag(models.Model):
return f'#{self.name}' 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): class Team(models.Model):
name = models.CharField(max_length=120) 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') 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): def get_absolute_url(self):
return reverse('item_detail', args=[self.pk]) 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): def sync_metadata(self):
tag_names = {m.group(1).lower() for m in TAG_RE.finditer(self.content)} 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] 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): class UserPreference(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='preferences') 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_notes = models.BooleanField(default=True)
show_open_todos = models.BooleanField(default=True) show_open_todos = models.BooleanField(default=True)
show_done_todos = models.BooleanField(default=False) show_done_todos = models.BooleanField(default=False)
show_links = models.BooleanField(default=True) show_links = models.BooleanField(default=True)
show_journal = models.BooleanField(default=False) show_journal = models.BooleanField(default=False)
due_first = models.BooleanField(default=True)
overview_lines = models.PositiveSmallIntegerField(default=4) overview_lines = models.PositiveSmallIntegerField(default=4)
def __str__(self): 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="private" {% if item.visibility == 'private' %}selected{% endif %}>{% trans "Private" %}</option>
<option value="public" {% if item.visibility == 'public' %}selected{% endif %}>{% trans "Public" %}</option> <option value="public" {% if item.visibility == 'public' %}selected{% endif %}>{% trans "Public" %}</option>
</select></div> </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 class="col-md-4"><input class="form-control" type="file" name="files" multiple x-ref="fileInput" @change="previewFile($event)"></div>
</div> </div>
{% if item.kind == 'link' %}<input class="form-control" type="url" name="url" value="{{ item.url }}" placeholder="URL">{% endif %} {% 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 %} {% 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="card-body">
<div class="item-row d-flex justify-content-between align-items-start gap-3"> <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"> <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 %} {% 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> </div>
{% if item.comment %}<div class="border-start ps-2 mt-2 small content text-muted">{{ item.comment|markdown }}</div>{% endif %} {% 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.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.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 %} {% if item.attachments.all %}
+3 -3
View File
@@ -1,10 +1,10 @@
{% load querystring i18n %} {% load querystring i18n %}
<div id="tag-list" class="d-flex flex-wrap gap-2"{% if oob %} hx-swap-oob="true"{% endif %}> <div id="tag-list" class="d-flex flex-wrap gap-2"{% if oob %} hx-swap-oob="true"{% endif %}>
{% for tag in tags %} {% for tag in tags %}
{% if active_tag == tag.name %} {% if tag.name in active_tags %}
<a class="badge rounded-pill text-bg-dark text-decoration-none tag" href="{% query_replace tag=None %}">#{{ tag.name }} ×</a> <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 %} {% 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 %} {% endif %}
{% empty %}<span class="text-muted small">{% trans "No tags yet" %}</span>{% endfor %} {% empty %}<span class="text-muted small">{% trans "No tags yet" %}</span>{% endfor %}
</div> </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 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> <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style> <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> </style>
</head> </head>
<body> <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> <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> <script>
document.body.addEventListener('htmx:configRequest', e => {e.detail.headers['X-CSRFToken']='{{ csrf_token }}'}); 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()}}} 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)})} 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('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')}}); 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(()=>{}));} if('serviceWorker' in navigator){window.addEventListener('load',()=>navigator.serviceWorker.register('/sw.js').catch(()=>{}));}
</script> </script>
+16 -5
View File
@@ -9,17 +9,19 @@
</div> </div>
<div class="row g-4"> <div class="row g-4">
<aside class="col-lg-3 order-lg-2"> <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="fw-semibold mb-2">{% trans "Filter" %}</div>
<div class="d-flex flex-wrap gap-2 mb-3"> <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 == '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 == '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_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 == '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> <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> </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(); }"> <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_kind %}<input type="hidden" name="kind" value="{{ active_kind }}">{% endif %}
{% if active_status %}<input type="hidden" name="status" value="{{ active_status }}">{% endif %} {% if active_status %}<input type="hidden" name="status" value="{{ active_status }}">{% endif %}
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
@@ -31,20 +33,29 @@
</div> </div>
</form> </form>
<div class="d-none d-md-block"> <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"> <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' %} {% include 'core/_tags.html' %}
</div> </div>
</div> </div>
<details class="d-md-none"> <details class="d-md-none">
<summary class="btn btn-sm btn-outline-secondary mb-2">{% trans "Show tags" %}</summary> <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"> <div class="d-flex flex-wrap gap-2">
{% for tag in tags %} {% 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 %} {% empty %}<span class="text-muted small">{% trans "No tags yet" %}</span>{% endfor %}
</div> </div>
</details> </details>
</div></div> </div>
</aside> </aside>
<section class="col-lg-9"> <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()}"> <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>
<div class="row g-2"> <div class="row g-2">
<div class="col-md-4"><select class="form-select" name="visibility"><option value="private" {% if item.visibility == 'private' %}selected{% endif %}>{% trans "Private" %}</option><option value="public" {% if item.visibility == 'public' %}selected{% endif %}>{% trans "Public" %}</option></select></div> <div class="col-md-4"><select class="form-select" name="visibility"><option value="private" {% if item.visibility == 'private' %}selected{% endif %}>{% trans "Private" %}</option><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 class="col-md-4"><input class="form-control" type="file" name="files" multiple x-ref="fileInput" @change="previewFile($event)"></div>
</div> </div>
{% if item.kind == 'link' %}<input class="form-control" type="url" name="url" value="{{ item.url }}" placeholder="URL">{% endif %} {% 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 %} {% block content %}
<div class="row g-4"> <div class="row g-4">
<aside class="col-lg-3 order-lg-2"> <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"> <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> <a class="btn btn-sm btn-outline-dark" href="?year={{ prev_year }}&month={{ prev_month }}">←</a>
<strong>{{ month }}/{{ year }}</strong> <strong>{{ month }}/{{ year }}</strong>
<a class="btn btn-sm btn-outline-dark" href="?year={{ next_year }}&month={{ next_month }}">→</a> <a class="btn btn-sm btn-outline-dark" href="?year={{ next_year }}&month={{ next_month }}">→</a>
</div> </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 %} {% 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> </tbody></table>
<div class="fw-semibold mb-2">{% trans "Tags" %}</div> <div class="fw-semibold mb-2">{% trans "Tags" %}</div>
{% include 'core/_tags.html' %} {% include 'core/_tags.html' %}
</div></div> </div>
</aside> </aside>
<section class="col-lg-9"> <section class="col-lg-9">
<h1 class="h4 mb-3">Journal {{ selected_day }}</h1> <h1 class="h4 mb-3">Journal {{ selected_day }}</h1>
+62 -35
View File
@@ -2,60 +2,87 @@
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<a class="btn btn-sm btn-outline-secondary mb-3" href="/">← {% trans "back" %}</a> <a class="btn btn-sm btn-outline-secondary mb-3" href="/">← {% trans "back" %}</a>
<div class="row g-4"> <div class="row g-4 align-items-start">
<section class="col-lg-6"> <aside class="col-lg-3">
<div class="card item-card shadow-sm"><div class="card-body"> <div class="card item-card shadow-sm sticky-lg-top" style="top:5rem"><div class="card-body">
<h1 class="h4 mb-3">{% trans "User settings" %}</h1> <h1 class="h5 mb-3">{% trans "Settings" %}</h1>
<form method="post" class="vstack gap-3"> <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"> {% csrf_token %}<input type="hidden" name="action" value="profile">
<div><label class="form-label">{% trans "First name" %}</label>{{ profile_form.first_name }}</div> <div class="col-md-6"><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 class="col-md-6"><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> <div class="col-12"><label class="form-label">{% trans "Email" %}</label>{{ profile_form.email }}</div>
<button class="btn btn-dark">{% trans "Save" %}</button> <div class="col-12"><button class="btn btn-dark">{% trans "Save profile" %}</button></div>
</form> </form>
</div></div> </div></div>
</section>
<section class="col-lg-6"> <div id="preferences" class="card item-card shadow-sm"><div class="card-body">
<div class="card item-card shadow-sm mb-3"><div class="card-body"> <h2 class="h4 mb-1">{% trans "View & language" %}</h2>
<h2 class="h5 mb-3">{% trans "Default view" %}</h2> <p class="text-muted small mb-3">{% trans "Choose your language and configure the default overview." %}</p>
<form method="post" class="vstack gap-2"> <form method="post" class="vstack gap-3">
{% csrf_token %}<input type="hidden" name="action" value="preferences"> {% csrf_token %}<input type="hidden" name="action" value="preferences">
<div class="row g-3">
{% for field in preference_form %} {% for field in preference_form %}
{% if field.field.widget.input_type == 'checkbox' %} {% if field.name == 'language' or field.name == 'overview_lines' %}
<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> <div class="col-md-6"><label class="form-label">{{ field.label }}</label>{{ field }}</div>
{% else %}
<div><label class="form-label">{{ field.label }}</label>{{ field }}</div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<button class="btn btn-dark mt-2">{% trans "Save default view" %}</button> <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> </form>
</div></div> </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> <div id="invites" class="card item-card shadow-sm"><div class="card-body">
<form method="post" class="vstack gap-3 mb-3"> <h2 class="h4 mb-1">{% trans "User invitations" %}</h2>
{% csrf_token %}<input type="hidden" name="action" value="team"> <p class="text-muted small mb-3">{% trans "Invite new users during the test phase. Only people with an invitation link can register." %}</p>
<div><label class="form-label">{% trans "Team name" %}</label>{{ team_form.name }}</div> <form method="post" class="row g-3 mb-4">
<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> {% csrf_token %}<input type="hidden" name="action" value="userinvite">
<button class="btn btn-dark">{% trans "Create team" %}</button> <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> </form>
<div class="vstack gap-2"> <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> </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> <div id="api" class="card item-card shadow-sm"><div class="card-body">
<form method="post" class="vstack gap-3"> <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"> {% 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 "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> <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> </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"> <div class="vstack gap-2">
{% for key in api_keys %} {% 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> <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> <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> <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' %} {% extends 'core/base.html' %}
{% load i18n %} {% load i18n %}
{% block content %} {% 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 class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div> <div>
<h1 class="h3 mb-1">{% trans "Team" %} {{ team.name }}</h1> <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('', views.index, name='index'),
path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'), path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'), 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('new/', views.new_item, name='new_item'),
path('journal/', views.journal, name='journal'), path('journal/', views.journal, name='journal'),
path('kanban/', views.kanban_list, name='kanban_list'), path('kanban/', views.kanban_list, name='kanban_list'),
path('kanban/<int:pk>/', views.kanban_detail, name='kanban_detail'), 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('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/', views.settings_view, name='settings'),
path('settings/api-keys/<int:pk>/delete/', views.delete_api_key, name='delete_api_key'), 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/<int:pk>/', views.team_detail, name='team_detail'),
path('teams/invite/<str:token>/', views.team_accept_invite, name='team_accept_invite'), path('teams/invite/<str:token>/', views.team_accept_invite, name='team_accept_invite'),
path('items/<int:pk>/', views.item_detail, name='item_detail'), path('items/<int:pk>/', views.item_detail, name='item_detail'),
+148 -28
View File
@@ -2,21 +2,24 @@ import json
import re import re
from functools import wraps from functools import wraps
from calendar import Calendar from calendar import Calendar
from datetime import date from datetime import date, datetime, time
from html import unescape from html import unescape
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required 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.http import HttpResponse, JsonResponse
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils import translation
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods, require_POST from django.views.decorators.http import require_http_methods, require_POST
from .forms import ApiKeyForm, EditItemForm, KanbanForm, QuickItemForm, TeamForm, TeamInviteForm, UserPreferenceForm, UserSettingsForm from .forms import ApiKeyForm, EditItemForm, InviteRegistrationForm, KanbanForm, QuickItemForm, TeamForm, TeamInviteForm, UserInviteForm, UserPreferenceForm, UserSettingsForm
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
COMMAND_RE = re.compile(r'/(todo|note|link|journal|public|private)\b', re.I) COMMAND_RE = re.compile(r'/(todo|note|link|journal|public|private)\b', re.I)
URL_RE = re.compile(r'https?://\S+') URL_RE = re.compile(r'https?://\S+')
@@ -51,6 +54,19 @@ def parse_quick_content(raw):
return kind, visibility, content, url 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): def attach_files_and_replace_tokens(item, files):
replacements = {} replacements = {}
for file in files: for file in files:
@@ -154,7 +170,9 @@ def index(request):
response['HX-Trigger'] = 'tagsChanged' response['HX-Trigger'] = 'tagsChanged'
return response return response
return redirect('index') 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') kind = request.GET.get('kind')
status = request.GET.get('status') status = request.GET.get('status')
day = request.GET.get('day') day = request.GET.get('day')
@@ -165,14 +183,18 @@ def index(request):
query.pop('q', None) query.pop('q', None)
return redirect(f'{request.path}?{query.urlencode()}') return redirect(f'{request.path}?{query.urlencode()}')
items = visible_items(request.user) items = visible_items(request.user)
prefs, created = UserPreference.objects.get_or_create(user=request.user)
applied_preferences = False applied_preferences = False
if tag: if active_tags:
items = items.filter(tags__name=tag) 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): if kind in dict(Item.Kind.choices):
items = items.filter(kind=kind) items = items.filter(kind=kind)
elif not any([tag, status, q, day]): elif not any([tag, status, q, day]):
applied_preferences = True applied_preferences = True
prefs, _ = UserPreference.objects.get_or_create(user=request.user)
type_filter = Q() type_filter = Q()
if prefs.show_notes: if prefs.show_notes:
type_filter |= Q(kind=Item.Kind.NOTE) type_filter |= Q(kind=Item.Kind.NOTE)
@@ -189,15 +211,24 @@ def index(request):
items = items.filter(created_at__date=day) items = items.filter(created_at__date=day)
if status == 'open': if status == 'open':
items = items.filter(kind=Item.Kind.TODO, is_done=False) 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': elif status == 'archive':
items = items.filter(kind=Item.Kind.TODO, is_done=True) items = items.filter(kind=Item.Kind.TODO, is_done=True)
elif not applied_preferences: elif not applied_preferences:
items = items.exclude(kind=Item.Kind.TODO, is_done=True) items = items.exclude(kind=Item.Kind.TODO, is_done=True)
if q: if q:
items = items.filter(Q(content__icontains=q) | Q(url__icontains=q) | Q(tags__name__icontains=q)).distinct() 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', { 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)] 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) 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) 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', { return render(request, 'core/journal.html', {
'form': QuickItemForm(), 'items': items, 'tags': Tag.objects.all(), 'weeks': weeks, 'year': year, 'month': month, 'selected_day': selected_day, '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, '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 @login_required
def settings_view(request): def settings_view(request):
if request.method == 'POST': if request.method == 'POST':
@@ -271,7 +321,7 @@ def settings_view(request):
messages.success(request, _('Settings saved.')) messages.success(request, _('Settings saved.'))
return redirect('settings') return redirect('settings')
elif request.POST.get('action') == 'preferences': 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) pref_form = UserPreferenceForm(request.POST, instance=prefs)
if pref_form.is_valid(): if pref_form.is_valid():
pref_form.save() pref_form.save()
@@ -286,24 +336,22 @@ def settings_view(request):
key_form.save_m2m() key_form.save_m2m()
messages.success(request, _('API key created: %(token)s') % {'token': api_key.token}) messages.success(request, _('API key created: %(token)s') % {'token': api_key.token})
return redirect('settings') return redirect('settings')
elif request.POST.get('action') == 'team': elif request.POST.get('action') == 'userinvite':
team_form = TeamForm(request.POST) invite_form = UserInviteForm(request.POST)
if team_form.is_valid(): if invite_form.is_valid():
team = team_form.save(commit=False) invite = invite_form.save(commit=False)
team.owner = request.user invite.invited_by = request.user
team.save() invite.save()
TeamMembership.objects.create(team=team, user=request.user, role=TeamMembership.Role.OWNER) messages.success(request, _('Invitation link created: %(url)s') % {'url': request.build_absolute_uri(reverse("accept_user_invite", args=[invite.token]))})
Tag.objects.get_or_create(name=team.slug) return redirect('settings')
messages.success(request, _('Team %(team)s created. Use #%(slug)s for team items.') % {'team': team.name, 'slug': team.slug}) prefs, created = UserPreference.objects.get_or_create(user=request.user)
return redirect('team_detail', pk=team.pk)
prefs, _ = UserPreference.objects.get_or_create(user=request.user)
return render(request, 'core/settings.html', { return render(request, 'core/settings.html', {
'profile_form': UserSettingsForm(instance=request.user), 'profile_form': UserSettingsForm(instance=request.user),
'preference_form': UserPreferenceForm(instance=prefs), 'preference_form': UserPreferenceForm(instance=prefs),
'key_form': ApiKeyForm(), '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'), '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') 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 @login_required
def team_detail(request, pk): def team_detail(request, pk):
membership = get_object_or_404(TeamMembership.objects.select_related('team'), team_id=pk, user=request.user) 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': if request.method == 'POST':
form = EditItemForm(request.POST, request.FILES, instance=item) form = EditItemForm(request.POST, request.FILES, instance=item)
if form.is_valid(): 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')) attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata() item.sync_metadata()
apply_team_from_tags(item, request.user, force_private=item.visibility == Item.Visibility.PRIVATE) 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': if request.method == 'POST':
form = EditItemForm(request.POST, request.FILES, instance=item) form = EditItemForm(request.POST, request.FILES, instance=item)
if form.is_valid(): 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')) attach_files_and_replace_tokens(item, request.FILES.getlist('files'))
item.sync_metadata() item.sync_metadata()
apply_team_from_tags(item, request.user, force_private=item.visibility == Item.Visibility.PRIVATE) 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 @login_required
def tag_list(request): 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 @login_required
+339 -190
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,109 +17,143 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: core/forms.py:28 #: core/context_processors.py:9 core/context_processors.py:16
msgid "Write something … /todo /link /public #tag [[note:1]]" #: core/templates/core/index.html:16
msgstr "Schreibe etwas … /todo /link /public #tag [[note:1]]" msgid "Notes"
msgstr "Notizen"
#: core/forms.py:65 #: core/context_processors.py:9 core/context_processors.py:15
msgid "Show notes" #| msgid "Task"
msgstr "Notizen anzeigen" msgid "Tasks"
msgstr "Aufgaben"
#: core/forms.py:66 #: core/context_processors.py:9 core/context_processors.py:17
msgid "Show open tasks" #| msgid "Links:"
msgstr "Offene Aufgaben anzeigen" msgid "Links"
msgstr "Links"
#: core/forms.py:67 #: core/context_processors.py:18 core/models.py:98
msgid "Show completed tasks" #: core/templates/core/base.html:24
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
msgid "Journal" msgid "Journal"
msgstr "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 #: core/templates/core/item_editor.html:25
msgid "Private" msgid "Private"
msgstr "Privat" 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" msgid "Team"
msgstr "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 #: core/templates/core/item_editor.html:25
msgid "Public" msgid "Public"
msgstr "Öffentlich" 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" msgid "Inbox"
msgstr "Eingang" msgstr "Eingang"
#: core/models.py:94 #: core/models.py:114
msgid "In Progress" msgid "In Progress"
msgstr "In Bearbeitung" 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 #: core/templates/core/kanban_list.html:7
msgid "Done" msgid "Done"
msgstr "Erledigt" msgstr "Erledigt"
#: core/models.py:167
msgid "Browser default"
msgstr "Browser-Standard"
#: core/templates/core/_edit_form.html:5 #: core/templates/core/_edit_form.html:5
#: core/templates/core/item_editor.html:11 #: core/templates/core/item_editor.html:11
msgid "Type:" msgid "Type:"
msgstr "Typ:" 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/item_editor.html:13 core/templates/core/journal.html:29
#: core/templates/core/new_item.html:13 #: core/templates/core/new_item.html:13
msgid "Add tags" 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]]" msgstr "Kommentar, z.B. gleiche Aufgabe bereits erfasst [[12]]"
#: core/templates/core/_edit_form.html:23 #: core/templates/core/_edit_form.html:23
msgid "Date/time for todo" #: core/templates/core/item_editor.html:26
msgstr "Datum/Zeit für Todo" 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:" msgid "Image from clipboard:"
msgstr "Bild aus Zwischenablage:" 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:9
#: core/templates/core/item_editor.html:31 core/templates/core/journal.html:32 #: 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/new_item.html:10 core/templates/core/new_item.html:30
#: core/templates/core/settings.html:14
msgid "Save" msgid "Save"
msgstr "Speichern" msgstr "Speichern"
@@ -168,6 +207,14 @@ msgstr "weniger anzeigen"
msgid "show more" msgid "show more"
msgstr "mehr anzeigen" 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 #: core/templates/core/_item.html:17
msgid "Due:" msgid "Due:"
msgstr "Fällig:" msgstr "Fällig:"
@@ -180,23 +227,30 @@ msgstr "Links:"
msgid "Attachments:" msgid "Attachments:"
msgstr "Anhänge:" 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" msgid "No tags yet"
msgstr "Noch keine Tags" msgstr "Noch keine Tags"
#: core/templates/core/base.html:24 #: core/templates/core/accept_user_invite.html:6
msgid "Notes · Tasks · Links" #: core/templates/core/accept_user_invite.html:19
msgstr "Notizen · Aufgaben · Links" 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 #: core/templates/core/base.html:24
msgid "Kanban" msgid "Kanban"
msgstr "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" msgid "Teams"
msgstr "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" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
@@ -212,57 +266,45 @@ msgstr "Logout"
msgid "Login" msgid "Login"
msgstr "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 #: core/templates/core/new_item.html:9
msgid "Create task" msgid "Create task"
msgstr "Aufgabe erstellen" msgstr "Aufgabe erstellen"
#: core/templates/core/base.html:32 core/templates/core/index.html:100 #: core/templates/core/base.html:32 core/templates/core/index.html:111
#, fuzzy
#| msgid "Create link"
msgid "Save link" msgid "Save link"
msgstr "Link erstellen" msgstr "Link erstellen"
#: core/templates/core/base.html:32 core/templates/core/index.html:101 #: core/templates/core/base.html:32 core/templates/core/index.html:112
#, fuzzy
#| msgid "Create journal entry"
msgid "Journal entry" msgid "Journal entry"
msgstr "Journal-Eintrag erstellen" msgstr "Journal-Eintrag erstellen"
#: core/templates/core/base.html:32 core/templates/core/index.html:102 #: core/templates/core/base.html:32 core/templates/core/index.html:113
#, fuzzy
#| msgid "Public"
msgid "public" msgid "public"
msgstr "Öffentlich" msgstr "Öffentlich"
#: core/templates/core/base.html:32 core/templates/core/index.html:103 #: core/templates/core/base.html:32 core/templates/core/index.html:114
#, fuzzy
#| msgid "Private"
msgid "private" msgid "private"
msgstr "Privat" 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" msgid "Insert Markdown code block"
msgstr "Markdown Codeblock einfügen" 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" msgid "Search internal task/note"
msgstr "interne Aufgabe/Notiz suchen" msgstr "interne Aufgabe/Notiz suchen"
#: core/templates/core/base.html:32 core/templates/core/index.html:106 #: core/templates/core/base.html:32 core/templates/core/index.html:117
#, fuzzy
#| msgid "to insert images into the text."
msgid "Insert image from attachments" msgid "Insert image from attachments"
msgstr "können Bilder im Text eingefügt werden." msgstr "können Bilder im Text eingefügt werden."
#: core/templates/core/base.html:33 core/templates/core/index.html:113 #: core/templates/core/base.html:33 core/templates/core/index.html:124
#, fuzzy
#| msgid "Image preview:"
msgid "Image:" msgid "Image:"
msgstr "Bildvorschau:" msgstr "Bildvorschau:"
#: core/templates/core/detail.html:4 core/templates/core/kanban_list.html:4 #: 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" msgid "back"
msgstr "zurück" msgstr "zurück"
@@ -270,50 +312,63 @@ msgstr "zurück"
msgid "Filter" msgid "Filter"
msgstr "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 #: core/templates/core/index.html:18 core/templates/core/kanban_list.html:7
msgid "Open" msgid "Open"
msgstr "Offen" msgstr "Offen"
#: core/templates/core/index.html:19 #: core/templates/core/index.html:19
msgid "Due"
msgstr "Fällig:"
#: core/templates/core/index.html:20
msgid "Archive" msgid "Archive"
msgstr "Archiv" msgstr "Archiv"
#: core/templates/core/index.html:26 #: core/templates/core/index.html:28
msgid "Search (Ctrl+F, # for tags)" msgid "Search (Ctrl+F, # for tags)"
msgstr "Suche (Ctrl+F, # für Tags)" msgstr "Suche (Ctrl+F, # für Tags)"
#: core/templates/core/index.html:27 #: core/templates/core/index.html:29
msgid "Clear search" msgid "Clear search"
msgstr "Suche löschen" msgstr "Suche löschen"
#: core/templates/core/index.html:34 core/templates/core/journal.html:15 #: core/templates/core/index.html:37 core/templates/core/journal.html:15
#: core/templates/core/settings.html:61 #: core/templates/core/settings.html:88
msgid "Tags" msgid "Tags"
msgstr "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" msgid "Show tags"
msgstr "Tags anzeigen" 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 #: core/templates/core/new_item.html:26
msgid "Gallery" msgid "Gallery"
msgstr "Galerie" 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 #: core/templates/core/new_item.html:26
msgid "Photo" msgid "Photo"
msgstr "Foto" msgstr "Foto"
#: core/templates/core/index.html:66 #: core/templates/core/index.html:77
msgid "Editor" msgid "Editor"
msgstr "Editor" msgstr "Editor"
#: core/templates/core/index.html:72 #: core/templates/core/index.html:83
msgid "Nothing here yet." msgid "Nothing here yet."
msgstr "Noch nichts vorhanden." msgstr "Noch nichts vorhanden."
@@ -382,71 +437,104 @@ msgstr "Mit"
msgid "to insert images into the text." msgid "to insert images into the text."
msgstr "können Bilder im Text eingefügt werden." msgstr "können Bilder im Text eingefügt werden."
#: core/templates/core/settings.html:8 #: core/templates/core/settings.html:10 core/templates/core/settings.html:20
msgid "User settings" msgid "Profile"
msgstr "Benutzereinstellungen" msgstr "Benutzer"
#: core/templates/core/settings.html:11 #: core/templates/core/settings.html:11 core/templates/core/settings.html:32
msgid "First name" msgid "View & language"
msgstr "Vorname" msgstr "Ansicht & Sprache"
#: core/templates/core/settings.html:12 #: core/templates/core/settings.html:12
msgid "Last name" msgid "Invitations"
msgstr "Name" msgstr "Offene Einladungen:"
#: core/templates/core/settings.html:20 #: core/templates/core/settings.html:13 core/templates/core/settings.html:75
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
msgid "API keys" msgid "API keys"
msgstr "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" msgid "Delete"
msgstr "Löschen" msgstr "Löschen"
#: core/templates/core/settings.html:61 #: core/templates/core/settings.html:70 core/templates/core/team_detail.html:29
msgid "all" msgid "No open invitations."
msgstr "alle" 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." msgid "No API keys yet."
msgstr "Noch keine API Keys." msgstr "Noch keine API Keys."
@@ -467,18 +555,10 @@ msgstr "Mitglieder"
msgid "Invite" msgid "Invite"
msgstr "Einladen" msgstr "Einladen"
#: core/templates/core/team_detail.html:26
msgid "Create invitation link"
msgstr "Einladungslink erzeugen"
#: core/templates/core/team_detail.html:28 #: core/templates/core/team_detail.html:28
msgid "Open invitations:" msgid "Open invitations:"
msgstr "Offene Einladungen:" 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 #: core/templates/core/team_detail.html:34
msgid "Team items" msgid "Team items"
msgstr "Team-Einträge" msgstr "Team-Einträge"
@@ -488,30 +568,99 @@ msgstr "Team-Einträge"
msgid "No team items yet. Create something with #%(slug)s." msgid "No team items yet. Create something with #%(slug)s."
msgstr "Noch keine Team-Einträge. Erstelle etwas mit #%(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." msgid "Settings saved."
msgstr "Einstellungen gespeichert." msgstr "Einstellungen gespeichert."
#: core/views.py:278 #: core/views.py:328
msgid "Default view saved." msgid "Default view saved."
msgstr "Standardansicht gespeichert." msgstr "Standardansicht gespeichert."
#: core/views.py:287 #: core/views.py:337
#, python-format #, python-format
msgid "API key created: %(token)s" msgid "API key created: %(token)s"
msgstr "API Key erzeugt: %(token)s" msgstr "API Key erzeugt: %(token)s"
#: core/views.py:297 #: core/views.py:345 core/views.py:439
#, 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
#, python-format #, python-format
msgid "Invitation link created: %(url)s" msgid "Invitation link created: %(url)s"
msgstr "Einladungslink erzeugt: %(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 #, python-format
msgid "You are now a member of team %(team)s." msgid "You are now a member of team %(team)s."
msgstr "Du bist jetzt Mitglied im 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,109 +18,140 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: core/forms.py:28 #: core/context_processors.py:9 core/context_processors.py:16
msgid "Write something … /todo /link /public #tag [[note:1]]" #: core/templates/core/index.html:16
msgid "Notes"
msgstr "" msgstr ""
#: core/forms.py:65 #: core/context_processors.py:9 core/context_processors.py:15
msgid "Show notes" msgid "Tasks"
msgstr "" msgstr ""
#: core/forms.py:66 #: core/context_processors.py:9 core/context_processors.py:17
msgid "Show open tasks" msgid "Links"
msgstr "" msgstr ""
#: core/forms.py:67 #: core/context_processors.py:18 core/models.py:98
msgid "Show completed tasks" #: core/templates/core/base.html:24
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
msgid "Journal" msgid "Journal"
msgstr "" 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 #: core/templates/core/item_editor.html:25
msgid "Private" msgid "Private"
msgstr "" 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" msgid "Team"
msgstr "" 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 #: core/templates/core/item_editor.html:25
msgid "Public" msgid "Public"
msgstr "" 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" msgid "Inbox"
msgstr "" msgstr ""
#: core/models.py:94 #: core/models.py:114
msgid "In Progress" msgid "In Progress"
msgstr "" 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 #: core/templates/core/kanban_list.html:7
msgid "Done" msgid "Done"
msgstr "" msgstr ""
#: core/models.py:167
msgid "Browser default"
msgstr ""
#: core/templates/core/_edit_form.html:5 #: core/templates/core/_edit_form.html:5
#: core/templates/core/item_editor.html:11 #: core/templates/core/item_editor.html:11
msgid "Type:" msgid "Type:"
msgstr "" 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/item_editor.html:13 core/templates/core/journal.html:29
#: core/templates/core/new_item.html:13 #: core/templates/core/new_item.html:13
msgid "Add tags" msgid "Add tags"
@@ -136,18 +167,23 @@ msgid "Comment, e.g. same task already captured [[12]]"
msgstr "" msgstr ""
#: core/templates/core/_edit_form.html:23 #: core/templates/core/_edit_form.html:23
msgid "Date/time for todo" #: core/templates/core/item_editor.html:26
msgid "Due date"
msgstr "" 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:" msgid "Image from clipboard:"
msgstr "" 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:9
#: core/templates/core/item_editor.html:31 core/templates/core/journal.html:32 #: 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/new_item.html:10 core/templates/core/new_item.html:30
#: core/templates/core/settings.html:14
msgid "Save" msgid "Save"
msgstr "" msgstr ""
@@ -169,6 +205,14 @@ msgstr ""
msgid "show more" msgid "show more"
msgstr "" 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 #: core/templates/core/_item.html:17
msgid "Due:" msgid "Due:"
msgstr "" msgstr ""
@@ -181,23 +225,29 @@ msgstr ""
msgid "Attachments:" msgid "Attachments:"
msgstr "" 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" msgid "No tags yet"
msgstr "" msgstr ""
#: core/templates/core/base.html:24 #: core/templates/core/accept_user_invite.html:6
msgid "Notes · Tasks · Links" #: 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 "" msgstr ""
#: core/templates/core/base.html:24 #: core/templates/core/base.html:24
msgid "Kanban" msgid "Kanban"
msgstr "" 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" msgid "Teams"
msgstr "" 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" msgid "Settings"
msgstr "" msgstr ""
@@ -213,45 +263,45 @@ msgstr ""
msgid "Login" msgid "Login"
msgstr "" 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 #: core/templates/core/new_item.html:9
msgid "Create task" msgid "Create task"
msgstr "" 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" msgid "Save link"
msgstr "" 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" msgid "Journal entry"
msgstr "" 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" msgid "public"
msgstr "" 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" msgid "private"
msgstr "" 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" msgid "Insert Markdown code block"
msgstr "" 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" msgid "Search internal task/note"
msgstr "" 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" msgid "Insert image from attachments"
msgstr "" 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:" msgid "Image:"
msgstr "" msgstr ""
#: core/templates/core/detail.html:4 core/templates/core/kanban_list.html:4 #: 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" msgid "back"
msgstr "" msgstr ""
@@ -259,50 +309,63 @@ msgstr ""
msgid "Filter" msgid "Filter"
msgstr "" msgstr ""
#: core/templates/core/index.html:16
msgid "Notes"
msgstr ""
#: core/templates/core/index.html:18 core/templates/core/kanban_list.html:7 #: core/templates/core/index.html:18 core/templates/core/kanban_list.html:7
msgid "Open" msgid "Open"
msgstr "" msgstr ""
#: core/templates/core/index.html:19 #: core/templates/core/index.html:19
msgid "Due"
msgstr ""
#: core/templates/core/index.html:20
msgid "Archive" msgid "Archive"
msgstr "" msgstr ""
#: core/templates/core/index.html:26 #: core/templates/core/index.html:28
msgid "Search (Ctrl+F, # for tags)" msgid "Search (Ctrl+F, # for tags)"
msgstr "" msgstr ""
#: core/templates/core/index.html:27 #: core/templates/core/index.html:29
msgid "Clear search" msgid "Clear search"
msgstr "" msgstr ""
#: core/templates/core/index.html:34 core/templates/core/journal.html:15 #: core/templates/core/index.html:37 core/templates/core/journal.html:15
#: core/templates/core/settings.html:61 #: core/templates/core/settings.html:88
msgid "Tags" msgid "Tags"
msgstr "" 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" msgid "Show tags"
msgstr "" 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 #: core/templates/core/new_item.html:26
msgid "Gallery" msgid "Gallery"
msgstr "" 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 #: core/templates/core/new_item.html:26
msgid "Photo" msgid "Photo"
msgstr "" msgstr ""
#: core/templates/core/index.html:66 #: core/templates/core/index.html:77
msgid "Editor" msgid "Editor"
msgstr "" msgstr ""
#: core/templates/core/index.html:72 #: core/templates/core/index.html:83
msgid "Nothing here yet." msgid "Nothing here yet."
msgstr "" msgstr ""
@@ -371,71 +434,101 @@ msgstr ""
msgid "to insert images into the text." msgid "to insert images into the text."
msgstr "" msgstr ""
#: core/templates/core/settings.html:8 #: core/templates/core/settings.html:10 core/templates/core/settings.html:20
msgid "User settings" msgid "Profile"
msgstr "" msgstr ""
#: core/templates/core/settings.html:11 #: core/templates/core/settings.html:11 core/templates/core/settings.html:32
msgid "First name" msgid "View & language"
msgstr "" msgstr ""
#: core/templates/core/settings.html:12 #: core/templates/core/settings.html:12
msgid "Last name" msgid "Invitations"
msgstr "" msgstr ""
#: core/templates/core/settings.html:20 #: core/templates/core/settings.html:13 core/templates/core/settings.html:75
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
msgid "API keys" msgid "API keys"
msgstr "" msgstr ""
#: core/templates/core/settings.html:59 #: core/templates/core/settings.html:21
msgid "Delete" msgid "Manage your personal user data."
msgstr "" msgstr ""
#: core/templates/core/settings.html:61 #: core/templates/core/settings.html:24
msgid "all" 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 "" msgstr ""
#: core/templates/core/settings.html:63 #: 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." msgid "No API keys yet."
msgstr "" msgstr ""
@@ -456,18 +549,10 @@ msgstr ""
msgid "Invite" msgid "Invite"
msgstr "" msgstr ""
#: core/templates/core/team_detail.html:26
msgid "Create invitation link"
msgstr ""
#: core/templates/core/team_detail.html:28 #: core/templates/core/team_detail.html:28
msgid "Open invitations:" msgid "Open invitations:"
msgstr "" msgstr ""
#: core/templates/core/team_detail.html:29
msgid "No open invitations."
msgstr ""
#: core/templates/core/team_detail.html:34 #: core/templates/core/team_detail.html:34
msgid "Team items" msgid "Team items"
msgstr "" msgstr ""
@@ -477,30 +562,78 @@ msgstr ""
msgid "No team items yet. Create something with #%(slug)s." msgid "No team items yet. Create something with #%(slug)s."
msgstr "" 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." msgid "Settings saved."
msgstr "" msgstr ""
#: core/views.py:278 #: core/views.py:328
msgid "Default view saved." msgid "Default view saved."
msgstr "" msgstr ""
#: core/views.py:287 #: core/views.py:337
#, python-format #, python-format
msgid "API key created: %(token)s" msgid "API key created: %(token)s"
msgstr "" msgstr ""
#: core/views.py:297 #: core/views.py:345 core/views.py:439
#, python-format
msgid "Team %(team)s created. Use #%(slug)s for team items."
msgstr ""
#: core/views.py:329
#, python-format #, python-format
msgid "Invitation link created: %(url)s" msgid "Invitation link created: %(url)s"
msgstr "" 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 #, python-format
msgid "You are now a member of team %(team)s." msgid "You are now a member of team %(team)s."
msgstr "" msgstr ""
+2
View File
@@ -28,6 +28,7 @@ MIDDLEWARE = [
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'core.middleware.UserLanguageMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
@@ -41,6 +42,7 @@ TEMPLATES = [{
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'core.context_processors.overview_navigation',
]}, ]},
}] }]
WSGI_APPLICATION = 'my2dos.wsgi.application' WSGI_APPLICATION = 'my2dos.wsgi.application'