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