{% trans "Create account" %}
+{% trans "You were invited to my2dos. Create your account to continue." %}
+ +diff --git a/core/admin.py b/core/admin.py index 615ca03..d8867b2 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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) diff --git a/core/context_processors.py b/core/context_processors.py new file mode 100644 index 0000000..3c883b5 --- /dev/null +++ b/core/context_processors.py @@ -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} diff --git a/core/forms.py b/core/forms.py index 283eb90..8d9db24 100644 --- a/core/forms.py +++ b/core/forms.py @@ -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): diff --git a/core/middleware.py b/core/middleware.py new file mode 100644 index 0000000..2df8c6b --- /dev/null +++ b/core/middleware.py @@ -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 diff --git a/core/migrations/0012_userpreference_language.py b/core/migrations/0012_userpreference_language.py new file mode 100644 index 0000000..53c6c50 --- /dev/null +++ b/core/migrations/0012_userpreference_language.py @@ -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), + ), + ] diff --git a/core/migrations/0013_userinvite.py b/core/migrations/0013_userinvite.py new file mode 100644 index 0000000..10ff4f0 --- /dev/null +++ b/core/migrations/0013_userinvite.py @@ -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'], + }, + ), + ] diff --git a/core/migrations/0014_alter_userinvite_email.py b/core/migrations/0014_alter_userinvite_email.py new file mode 100644 index 0000000..8254ad6 --- /dev/null +++ b/core/migrations/0014_alter_userinvite_email.py @@ -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), + ), + ] diff --git a/core/migrations/0015_userpreference_due_first.py b/core/migrations/0015_userpreference_due_first.py new file mode 100644 index 0000000..baa0563 --- /dev/null +++ b/core/migrations/0015_userpreference_due_first.py @@ -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), + ), + ] diff --git a/core/models.py b/core/models.py index 07b77c2..628cf9f 100644 --- a/core/models.py +++ b/core/models.py @@ -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'(?{% trans "Private" %} - {% if item.kind == 'todo' %}
{% endif %} + {% if item.kind == 'todo' %}{% endif %} {% if item.kind == 'link' %}{% endif %} diff --git a/core/templates/core/_item.html b/core/templates/core/_item.html index e5fc7c3..f267785 100644 --- a/core/templates/core/_item.html +++ b/core/templates/core/_item.html @@ -1,5 +1,5 @@ {% load markdown_extras i18n %} -{% trans "You were invited to my2dos. Create your account to continue." %}
+ +