mirror of
https://github.com/bugsink/bugsink.git
synced 2025-12-21 13:00:13 -06:00
@@ -0,0 +1,20 @@
|
||||
from django.db import models
|
||||
|
||||
IGNORED_ATTRS = ['verbose_name', 'help_text']
|
||||
|
||||
original_deconstruct = models.Field.deconstruct
|
||||
|
||||
|
||||
def new_deconstruct(self):
|
||||
# works around the non-fix of https://code.djangoproject.com/ticket/21498 (I don't agree with the reasoning that
|
||||
# "in principle any field could influence the database schema"; you must be _insane_ if verbose_name or help_text
|
||||
# actually do, and the cost of the migrations is real)
|
||||
# solution from https://stackoverflow.com/a/39801321/339144
|
||||
name, path, args, kwargs = original_deconstruct(self)
|
||||
for attr in IGNORED_ATTRS:
|
||||
kwargs.pop(attr, None)
|
||||
return name, path, args, kwargs
|
||||
|
||||
|
||||
def monkey_patch_deconstruct():
|
||||
models.Field.deconstruct = new_deconstruct
|
||||
|
||||
8
bsmain/management/commands/makemigrations.py
Normal file
8
bsmain/management/commands/makemigrations.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.core.management.commands.makemigrations import Command as OriginalCommand
|
||||
|
||||
from . import monkey_patch_deconstruct
|
||||
monkey_patch_deconstruct()
|
||||
|
||||
|
||||
class Command(OriginalCommand):
|
||||
pass # no changes, except the monkey patch above
|
||||
@@ -1,6 +1,9 @@
|
||||
import time
|
||||
from django.core.management.commands.migrate import Command as DjangoMigrateCommand
|
||||
|
||||
from . import monkey_patch_deconstruct
|
||||
monkey_patch_deconstruct() # needed for migrate.py to avoid the warning about non-reflected changes
|
||||
|
||||
|
||||
class Command(DjangoMigrateCommand):
|
||||
# We override the default Django migrate command to add the elapsed time for each migration. (This could in theory
|
||||
@@ -10,8 +13,7 @@ class Command(DjangoMigrateCommand):
|
||||
# We care more about the elapsed time for each migration than the average Django user because sqlite takes such a
|
||||
# prominent role in our architecture, and because migrations are run out of our direct control ("self hosted").
|
||||
#
|
||||
# AFAIU, "just dropping a file called migrate.py in one of our apps" is good enough to be the override (and if it
|
||||
# isn't, it's not critical, since all we do is add a bit more info to the output).
|
||||
# AFAIU, "just dropping a file called migrate.py in one of our apps" is good enough to be the override.
|
||||
|
||||
def migration_progress_callback(self, action, migration=None, fake=False):
|
||||
# Django 4.2's method, with a single change
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Auth Tokens · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "Auth Tokens" %} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -19,12 +20,12 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="flex">
|
||||
<h1 class="text-4xl mt-4 font-bold">Auth Tokens</h1>
|
||||
<h1 class="text-4xl mt-4 font-bold">{% translate "Auth Tokens" %}</h1>
|
||||
|
||||
<div class="ml-auto mt-6">
|
||||
<form action="{% url "auth_token_create" %}" method="post">
|
||||
{% csrf_token %} {# margins display slightly different from the <a href version that I have for e.g. project memembers, but I don't care _that_ much #}
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Add Token</button>
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Add Token" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,7 +38,7 @@
|
||||
<tbody>
|
||||
<thead>
|
||||
<tr class="bg-slate-200 dark:bg-slate-800">
|
||||
<th class="w-full p-4 text-left text-xl" colspan="2">Auth Tokens</th>
|
||||
<th class="w-full p-4 text-left text-xl" colspan="2">{% translate "Auth Tokens" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for auth_token in auth_tokens %}
|
||||
@@ -50,7 +51,7 @@
|
||||
|
||||
<td class="p-4">
|
||||
<div class="flex justify-end">
|
||||
<button name="action" value="delete:{{ auth_token.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Delete</button>
|
||||
<button name="action" value="delete:{{ auth_token.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Delete" %}</button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -59,7 +60,7 @@
|
||||
<tr class="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 border-b-2">
|
||||
<td class="w-full p-4">
|
||||
<div>
|
||||
No Auth Tokens.
|
||||
{% translate "No Auth Tokens." %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.shortcuts import render, redirect
|
||||
from django.http import Http404
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bugsink.decorators import atomic_for_request_method
|
||||
|
||||
@@ -20,7 +21,7 @@ def auth_token_list(request):
|
||||
if action == "delete":
|
||||
AuthToken.objects.get(pk=pk).delete()
|
||||
|
||||
messages.success(request, 'Token deleted')
|
||||
messages.success(request, _('Token deleted'))
|
||||
return redirect('auth_token_list')
|
||||
|
||||
return render(request, 'bsmain/auth_token_list.html', {
|
||||
|
||||
@@ -5,6 +5,10 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.db import connection
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.translation import get_supported_language_variant
|
||||
from django.utils.translation.trans_real import parse_accept_lang_header
|
||||
from django.utils import translation
|
||||
|
||||
|
||||
performance_logger = logging.getLogger("bugsink.performance.views")
|
||||
|
||||
@@ -128,3 +132,35 @@ class SetRemoteAddrMiddleware:
|
||||
request.META["REMOTE_ADDR"] = self.parse_x_forwarded_for(request.META.get("HTTP_X_FORWARDED_FOR", None))
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
def language_from_accept_language(request):
|
||||
"""
|
||||
Pick a language using ONLY the Accept-Language header. Ignores URL prefixes, session, and cookies. I prefer to have
|
||||
as little "magic" in the language selection as possible, and I _know_ we don't do anything with paths, so I'd rather
|
||||
not have such code invoked at all (at the cost of reimplementing some of Django's logic here).
|
||||
"""
|
||||
header = request.META.get("HTTP_ACCEPT_LANGUAGE", "")
|
||||
for lang_code, _q in parse_accept_lang_header(header):
|
||||
try:
|
||||
# strict=False lets country variants match (e.g. 'es-CO' for 'es')
|
||||
return get_supported_language_variant(lang_code, strict=False)
|
||||
except LookupError:
|
||||
continue
|
||||
return settings.LANGUAGE_CODE
|
||||
|
||||
|
||||
def get_chosen_language(request_user, request):
|
||||
if request_user.is_authenticated and request_user.language != "auto":
|
||||
return get_supported_language_variant(request_user.language, strict=False)
|
||||
return language_from_accept_language(request)
|
||||
|
||||
|
||||
class UserLanguageMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
translation.activate(get_chosen_language(request.user, request))
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
@@ -103,6 +103,10 @@ MIDDLEWARE = [
|
||||
|
||||
'bugsink.middleware.LoginRequiredMiddleware',
|
||||
|
||||
# note on ordering: we need request.user, so after AuthenticationMiddleware; and we're not tied to "before
|
||||
# CommonMiddleware" as django.middleware.locale.LocaleMiddleware is, because we don't do path-related stuff.
|
||||
'bugsink.middleware.UserLanguageMiddleware',
|
||||
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
|
||||
@@ -241,6 +245,14 @@ TIME_ZONE = 'Europe/Amsterdam'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
LOCALE_PATHS = [BASE_DIR / "locale"]
|
||||
LANGUAGES = (
|
||||
("en", "English"),
|
||||
("zh-hans", "简体中文"),
|
||||
)
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.db.models.functions import Concat
|
||||
from django.template.defaultfilters import date as default_date_filter
|
||||
from django.conf import settings
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bugsink.utils import assert_
|
||||
from bugsink.volume_based_condition import VolumeBasedCondition
|
||||
@@ -485,16 +486,16 @@ class IssueQuerysetStateManager(object):
|
||||
class TurningPointKind(models.IntegerChoices):
|
||||
# The language of the kinds reflects a historic view of the system, e.g. "first seen" as opposed to "new issue"; an
|
||||
# alternative take (which is more consistent with the language used elsewhere" is a more "active" language.
|
||||
FIRST_SEEN = 1, "First seen"
|
||||
RESOLVED = 2, "Resolved"
|
||||
MUTED = 3, "Muted"
|
||||
REGRESSED = 4, "Marked as regressed"
|
||||
UNMUTED = 5, "Unmuted"
|
||||
FIRST_SEEN = 1, _("First seen")
|
||||
RESOLVED = 2, _("Resolved")
|
||||
MUTED = 3, _("Muted")
|
||||
REGRESSED = 4, _("Marked as regressed")
|
||||
UNMUTED = 5, _("Unmuted")
|
||||
|
||||
NEXT_MATERIALIZED = 10, "Release info added"
|
||||
NEXT_MATERIALIZED = 10, _("Release info added")
|
||||
|
||||
# ASSGINED = 10, "Assigned to user" # perhaps later
|
||||
MANUAL_ANNOTATION = 100, "Manual annotation"
|
||||
MANUAL_ANNOTATION = 100, _("Manual annotation")
|
||||
|
||||
|
||||
class TurningPoint(models.Model):
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{% load add_to_qs %}
|
||||
{% load i18n %}
|
||||
|
||||
<form action="{% url this_view issue_pk=issue.pk nav="last" %}" method="get">{# nav="last": when doing a new search on an event-page, you want the most recent matching event to show up #}
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="search..." class="focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md mr-2"/>
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="{% translate 'search...' %}" class="focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md mr-2"/>
|
||||
</form>
|
||||
|
||||
{% if has_prev %} {# no need for 'is_first': if you can go to the left, you can go all the way to the left too #}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
{% load humanize %}
|
||||
{% load stricter_templates %}
|
||||
{% load add_to_qs %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ issue.title }} · {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -18,12 +20,12 @@
|
||||
|
||||
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
|
||||
{% if issue.project.has_releases %}
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="resolved_next">Resolved in next release</button>
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="resolved_next">{% translate "Resolved in next release" %}</button>
|
||||
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 stroke-slate-300 dark:stroke-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-t-2 border-b-2 rounded-e-md"><svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
|
||||
{# we just hide the whole dropdown; this is the easiest implementation of not-showing the dropdown #}
|
||||
{% else %}
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-md" name="action" value="resolved">Resolve</button>
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-md" name="action" value="resolved">{% translate "Resolve" %}</button>
|
||||
{% endif %}
|
||||
{% endspaceless %}
|
||||
|
||||
@@ -32,7 +34,7 @@
|
||||
|
||||
{% if issue.project.has_releases %}
|
||||
{# 'by next' is shown even if 'by current' is also shown: just because you haven't seen 'by current' doesn't mean it's actually already solved; and in fact we show this option first precisely because we can always show it #}
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-s-md" name="action" value="resolved_next">Resolved in next release</button>
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-s-md" name="action" value="resolved_next">{% translate "Resolved in next release" %}</button>
|
||||
|
||||
<div class="dropdown">
|
||||
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-800 dark:text-slate-100 fill-slate-800 dark:fill-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-r-2 border-t-2 border-b-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md"><svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
|
||||
@@ -51,7 +53,7 @@
|
||||
|
||||
|
||||
{% else %}
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="resolve">Resolve</button>
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="resolve">{% translate "Resolve" %}</button>
|
||||
{% endif %}
|
||||
|
||||
{% endspaceless %}
|
||||
@@ -59,14 +61,14 @@
|
||||
|
||||
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
|
||||
{% if not issue.is_muted and not issue.is_resolved %}
|
||||
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-s-md" name="action" value="mute">Mute</button>
|
||||
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-s-md" name="action" value="mute">{% translate "Mute" %}</button>
|
||||
{% else %}
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="mute">Mute</button>
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="mute">{% translate "Mute" %}</button>
|
||||
{% endif %}
|
||||
|
||||
<div class="dropdown">
|
||||
{% if not issue.is_muted and not issue.is_resolved %}
|
||||
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">Mute for/until <svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
|
||||
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">{% translate "Mute for/until " %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
|
||||
|
||||
<div class="dropdown-content-right flex-col">
|
||||
{% for mute_option in mute_options %}
|
||||
@@ -74,7 +76,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2">Mute for/until <svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2">{% translate "Mute for/until " %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
|
||||
{# note that when the issue is muted, no further muting is allowed. this is a design decision, I figured this is the easiest-to-understand UI, #}
|
||||
{# both at the point-of-clicking and when displaying the when-will-this-be-unmuted in some place #}
|
||||
{# (the alternative would be to allow multiple simulteneous reasons for unmuting to exist next to each other #}
|
||||
@@ -83,9 +85,9 @@
|
||||
</div>
|
||||
|
||||
{% if issue.is_muted and not issue.is_resolved %}
|
||||
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md" name="action" value="unmute">Unmute</button>
|
||||
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
|
||||
{% else %}
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 rounded-e-md" name="action" value="unmute">Unmute</button>
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
|
||||
{% endif %}
|
||||
|
||||
{% endspaceless %}
|
||||
@@ -107,13 +109,13 @@
|
||||
{# overflow-x-auto is needed at the level of the flex item such that it works at the level where we need it (the code listings)#}
|
||||
<div class="ml-4 mb-4 mr-4 border-2 overflow-x-auto flex-[2_1_96rem]"><!-- the whole of the big tabbed view--> {# 96rem is 1536px, which matches the 2xl class; this is no "must" but eyeballing revealed: good result #}
|
||||
<div class="flex bg-slate-50 dark:bg-slate-800 border-b-2"><!-- container for the actual tab buttons -->
|
||||
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "stacktrace" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Stacktrace</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/details/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-details" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Event Details</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/breadcrumbs/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "breadcrumbs" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Breadcrumbs</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/events/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-list" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Event List</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/tags/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "tags" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Tags</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/grouping/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "grouping" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Grouping</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/history/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "history" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">History</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "stacktrace" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Stacktrace" %}</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/details/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-details" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Event Details" %}</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/breadcrumbs/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "breadcrumbs" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Breadcrumbs" %}</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/events/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-list" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Event List" %}</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/tags/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "tags" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Tags" %}</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/grouping/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "grouping" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Grouping" %}</div></a>
|
||||
<a href="/issues/issue/{{ issue.id }}/history/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "history" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "History" %}</div></a>
|
||||
</div>
|
||||
|
||||
<div class="m-4"><!-- div for tab_content -->
|
||||
@@ -122,19 +124,19 @@
|
||||
</div>
|
||||
|
||||
<div class="flex p-4 bg-slate-200 dark:bg-slate-800 border-b-2"><!-- bottom nav bar -->
|
||||
{% if is_event_page %}<div>Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }} which occured at <span class="font-bold">{{ event.ingested_at|date:"j M G:i T" }}</span></div>{% endif %}
|
||||
{% if is_event_page %}<div>{% blocktranslate with digest_order=event.digest_order|intcomma total_events=issue.digested_event_count|intcomma ingested_at=event.ingested_at|date:"j M G:i T" %}Event {{ digest_order }} of {{ total_events }} which occured at <span class="font-bold">{{ ingested_at }}</span>{% endblocktranslate %}</div>{% endif %}
|
||||
<div class="ml-auto pr-4 font-bold text-slate-500 dark:text-slate-300">
|
||||
{% if is_event_page %}
|
||||
<a href="/events/event/{{ event.id }}/download/">Download</a>
|
||||
| <a href="/events/event/{{ event.id }}/raw/" >JSON</a>
|
||||
| <a href="/events/event/{{ event.id }}/plain/" >Plain</a>
|
||||
<a href="/events/event/{{ event.id }}/download/">{% translate "Download" %}</a>
|
||||
| <a href="/events/event/{{ event.id }}/raw/" >{% translate "JSON" %}</a>
|
||||
| <a href="/events/event/{{ event.id }}/plain/" >{% translate "Plain" %}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if app_settings.USE_ADMIN and user.is_staff %}
|
||||
{% if is_event_page %}
|
||||
| <a href="/admin/events/event/{{ event.id }}/change/">Event Admin</a> |
|
||||
| <a href="/admin/events/event/{{ event.id }}/change/">{% translate "Event Admin" %}</a> |
|
||||
{% endif %}
|
||||
<a href="/admin/issues/issue/{{ issue.id }}/change/">Issue Admin</a>
|
||||
<a href="/admin/issues/issue/{{ issue.id }}/change/">{% translate "Issue Admin" %}</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
@@ -152,22 +154,22 @@
|
||||
|
||||
<div class="p-4">
|
||||
<div class="mb-4">
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">Issue #</div>
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "Issue" %} #</div>
|
||||
<div>{{ issue.friendly_id }} </div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">State</div>
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "State" %}</div>
|
||||
<div>
|
||||
{% if issue.is_resolved %}
|
||||
Resolved
|
||||
{% translate "Resolved" %}
|
||||
{% for version in issue.get_fixed_at %}
|
||||
{% if forloop.first %}in{% endif %}
|
||||
{% if forloop.first %}{% translate "in" %}{% endif %}
|
||||
<span {% if version|issha %}class="font-mono"{% endif %}>{{ version|shortsha }}{% if not forloop.last %}</span>,{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% if issue.is_muted %}
|
||||
Muted
|
||||
{% translate "Muted" %}
|
||||
{% if issue.unmute_after %}
|
||||
until {{ issue.unmute_after|date:"j M G:i T" }}.
|
||||
{% elif issue.get_unmute_on_volume_based_conditions %}
|
||||
@@ -178,14 +180,14 @@
|
||||
(unconditionally).
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Open
|
||||
{% translate "Open" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">Nr. of events:</div>
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "Nr. of events" %}:</div>
|
||||
<div>{{ issue.digested_event_count|intcomma }}
|
||||
{% if issue.digested_event_count != issue.stored_event_count %}
|
||||
total seen</div><div>{{ issue.stored_event_count|intcomma }} available</div>
|
||||
@@ -196,17 +198,17 @@
|
||||
|
||||
{% if issue.digested_event_count > 1 %}
|
||||
<div class="mb-4">
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">First seen:</div>
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "First seen" %}:</div>
|
||||
<div>{{ issue.first_seen|date:"j M G:i T" }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">Last seen:</div>
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "Last seen" %}:</div>
|
||||
<div>{{ issue.last_seen|date:"j M G:i T" }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-4">
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">Seen at:</div>
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "Seen at" %}:</div>
|
||||
<div>{{ issue.first_seen|date:"j M G:i T" }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -229,7 +231,7 @@
|
||||
<div class="border-2 mb-4 mr-4"><!-- "issue: tags" box -->
|
||||
<div class="font-bold border-b-2">
|
||||
<div class="p-4 border-slate-50 dark:border-slate-900 text-slate-500 dark:text-slate-300">
|
||||
Issue Tags
|
||||
{% translate "Issue Tags" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% load stricter_templates %}
|
||||
{% load issues %}
|
||||
{% load humanize %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block tab_content %}
|
||||
|
||||
@@ -21,7 +22,7 @@
|
||||
{% if not breadcrumbs %}
|
||||
|
||||
<div class="mt-6 mb-6 italic">
|
||||
No breadcrumbs available for this event.
|
||||
{% translate "No breadcrumbs available for this event." %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{% extends "issues/base.html" %}
|
||||
{% load static %}
|
||||
{% load user %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block tab_content %}
|
||||
|
||||
<h1 class="text-2xl font-bold text-ellipsis whitespace-nowrap overflow-hidden">History</h1>
|
||||
<div class="italic">Most recent first</div>
|
||||
<h1 class="text-2xl font-bold text-ellipsis whitespace-nowrap overflow-hidden">{% translate "History" %}</h1>
|
||||
<div class="italic">{% translate "Most recent first" %}</div>
|
||||
|
||||
|
||||
<div class="flex"><!-- single turningpoint (for 'your comments')-->
|
||||
@@ -17,12 +18,12 @@
|
||||
<div class="border-slate-300 dark:border-slate-600 border-2 rounded-md mt-6 flex-auto"><!-- the "your comments balloon" -->
|
||||
<div class="pl-4 flex triangle-left"><!-- 'header' row -->
|
||||
<div class="mt-4 mb-4">
|
||||
<span class="font-bold text-slate-800 dark:text-slate-100 italic">Add comment as manual annotation</span>
|
||||
<span class="font-bold text-slate-800 dark:text-slate-100 italic">{% translate "Add comment as manual annotation" %}</span>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex"> <!-- 'header' row right side -->
|
||||
<div class="p-4">
|
||||
Now
|
||||
{% translate "Now" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,8 +32,8 @@
|
||||
<div class="mt-4">
|
||||
<form action="{% url "history_comment_new" issue_pk=issue.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<textarea name="comment" placeholder="comments..." class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md w-full h-32" onkeypress="submitOnCtrlEnter(event)"></textarea>
|
||||
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 mt-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">Post comment</button>
|
||||
<textarea name="comment" placeholder="{% translate 'comments...' %}" class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md w-full h-32" onkeypress="submitOnCtrlEnter(event)"></textarea>
|
||||
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 mt-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">{% translate "Post comment" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>{# 'body' part of the balloon #}
|
||||
@@ -55,7 +56,7 @@
|
||||
<div class="border-slate-300 dark:border-slate-600 border-2 rounded-md mt-6 flex-auto js-balloon"><!-- the "balloon" -->
|
||||
<div class="pl-4 flex triangle-left"><!-- 'header' row -->
|
||||
<div class="mt-4 mb-4">
|
||||
<span class="font-bold text-slate-800 dark:text-slate-100">{{ turningpoint.get_kind_display }}</span> by
|
||||
<span class="font-bold text-slate-800 dark:text-slate-100">{{ turningpoint.get_kind_display }}</span> {% translate "by" context "History" %}
|
||||
<span class="font-bold text-slate-800 dark:text-slate-100">{% if turningpoint.user_id %}{{ turningpoint.user|best_displayname }}{% else %}Bugsink{% endif %}</span>
|
||||
|
||||
{% if turningpoint.user_id == request.user.id %}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
{% load static add_to_qs %}
|
||||
{% load humanize %}
|
||||
{% load add_to_qs %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Issues · {{ project.name }} · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "Issues" %} · {{ project.name }} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -11,15 +12,15 @@
|
||||
<div id="deleteModal" class="hidden fixed inset-0 bg-slate-600 dark:bg-slate-900 bg-opacity-50 dark:bg-opacity-50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
|
||||
<div class="relative p-6 border border-slate-300 dark:border-slate-600 w-96 shadow-lg rounded-md bg-white dark:bg-slate-900">
|
||||
<div class="text-center m-4">
|
||||
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">Delete Issues</h3>
|
||||
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">{% translate "Delete Issues" %}</h3>
|
||||
<div class="mt-4 mb-6">
|
||||
<p class="text-slate-700 dark:text-slate-300">
|
||||
Deleting an Issue is a permanent action and cannot be undone. It's typically better to resolve or mute an issue instead of deleting it, as this allows you to keep track of past issues and their resolutions.
|
||||
{% translate "Deleting an Issue is a permanent action and cannot be undone. It's typically better to resolve or mute an issue instead of deleting it, as this allows you to keep track of past issues and their resolutions." %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-center space-x-4 mb-4">
|
||||
<button id="cancelDelete" class="text-cyan-500 dark:text-cyan-300 font-bold">Cancel</button>
|
||||
<button id="confirmDelete" type="submit" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring">Delete</button>
|
||||
<button id="cancelDelete" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Cancel" %}</button>
|
||||
<button id="confirmDelete" type="submit" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring">{% translate "Delete" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,25 +28,25 @@
|
||||
|
||||
|
||||
<div class="m-4">
|
||||
<h1 class="text-4xl mt-4 font-bold">{{ project.name }} - Issues</h1>
|
||||
<h1 class="text-4xl mt-4 font-bold">{{ project.name }} - {% translate "Issues" %}</h1>
|
||||
|
||||
{% if unapplied_issue_ids %}
|
||||
<div class="bg-red-100 w-full mt-2 mb-2 p-4 border-red-800 border-2">
|
||||
The chosen action is not applicable to all selected issues. Issues for which it has not been applied have been left with checkboxes checked so that you can try again with another action.
|
||||
{% translate "The chosen action is not applicable to all selected issues. Issues for which it has not been applied have been left with checkboxes checked so that you can try again with another action." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex bg-slate-50 dark:bg-slate-800 border-b-2 mt-4 items-end">
|
||||
<div class="flex">
|
||||
<a href="{% url "issue_list_open" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "open" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">Open</div></a>
|
||||
<a href="{% url "issue_list_unresolved" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "unresolved" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">Unresolved</div></a>
|
||||
<a href="{% url "issue_list_muted" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "muted" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">Muted</div></a>
|
||||
<a href="{% url "issue_list_resolved" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "resolved" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">Resolved</div></a>
|
||||
<a href="{% url "issue_list_all" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "all" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">All</div></a>
|
||||
<a href="{% url "issue_list_open" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "open" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">{% translate "Open" %}</div></a>
|
||||
<a href="{% url "issue_list_unresolved" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "unresolved" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">{% translate "Unresolved" %}</div></a>
|
||||
<a href="{% url "issue_list_muted" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "muted" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">{% translate "Muted" %}</div></a>
|
||||
<a href="{% url "issue_list_resolved" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "resolved" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">{% translate "Resolved" %}</div></a>
|
||||
<a href="{% url "issue_list_all" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "all" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">{% translate "All" %}</div></a>
|
||||
</div>
|
||||
<div class="ml-auto p-2">
|
||||
<form action="." method="get">
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="search issues..." class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md"/>
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="{% translate 'Search issues...' %}" class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,12 +76,12 @@
|
||||
|
||||
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
|
||||
{% if project.has_releases %}
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="resolved_next">Resolved in next release</button>
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="resolved_next">{% translate "Resolved in next release" %}</button>
|
||||
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-t-2 border-b-2 rounded-e-md"><svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
|
||||
{# we just hide the whole dropdown; this is the easiest implementation of not-showing the dropdown #}
|
||||
{% else %}
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-md" name="action" value="resolved">Resolve</button>
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-md" name="action" value="resolved">{% translate "Resolve" %}</button>
|
||||
{% endif %}
|
||||
{% endspaceless %}
|
||||
|
||||
@@ -105,7 +106,7 @@
|
||||
|
||||
|
||||
{% else %}
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="resolve">Resolve</button>
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="resolve">{% translate "Resolve" %}</button>
|
||||
{% endif %}
|
||||
|
||||
{% endspaceless %}
|
||||
@@ -113,14 +114,14 @@
|
||||
|
||||
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
|
||||
{% if not disable_mute_buttons %}
|
||||
<button name="action" value="mute" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-s-md">Mute</button>
|
||||
<button name="action" value="mute" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-s-md">{% translate "Mute" %}</button>
|
||||
{% else %}
|
||||
<button disabled name="action" value="mute" class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md">Mute</button>
|
||||
<button disabled name="action" value="mute" class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md">{% translate "Mute" %}</button>
|
||||
{% endif %}
|
||||
|
||||
<div class="dropdown">
|
||||
{% if not disable_mute_buttons %}
|
||||
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">Mute for/until <svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
|
||||
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">{% translate "Mute for/until " %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
|
||||
|
||||
<div class="dropdown-content-right flex-col">
|
||||
{% for mute_option in mute_options %}
|
||||
@@ -128,22 +129,22 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2">Mute for/until <svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2">{% translate "Mute for/until " %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
|
||||
{# we just hide the whole dropdown; this is the easiest implementation of not-showing the dropdown when the issue is already muted #}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not disable_unmute_buttons %}
|
||||
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md" name="action" value="unmute">Unmute</button>
|
||||
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
|
||||
{% else %}
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 rounded-e-md" name="action" value="unmute">Unmute</button>
|
||||
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
|
||||
{% endif %}
|
||||
|
||||
<div class="dropdown">
|
||||
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 fill-slate-500 border-slate-300 ml-2 pl-4 pr-4 pb-2 pt-2 border-2 hover:bg-slate-200 active:ring rounded-md">...</button>
|
||||
|
||||
<div class="dropdown-content-right flex-col">
|
||||
<button type="button" onclick="showDeleteConfirmation()" class="block self-stretch font-bold text-red-500 dark:text-slate-300 border-slate-300 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-red-50 dark:hover:bg-red-800 active:ring text-left whitespace-nowrap">Delete</button>
|
||||
<button type="button" onclick="showDeleteConfirmation()" class="block self-stretch font-bold text-red-500 dark:text-slate-300 border-slate-300 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-red-50 dark:hover:bg-red-800 active:ring text-left whitespace-nowrap">{% translate "Delete" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +153,7 @@
|
||||
|
||||
|
||||
{# NOTE: "reopen" is not available in the UI as per the notes in issue_detail #}
|
||||
{# only for resolved/muted items <button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Reopen</button> #}
|
||||
{# only for resolved/muted items <button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Reopen" %}</button> #}
|
||||
|
||||
</div>
|
||||
</td>
|
||||
@@ -174,7 +175,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" />
|
||||
</svg> {% endif %}{{ issue.title|truncatechars:100 }}</a>
|
||||
</div>
|
||||
<div class="text-sm">from <b>{{ issue.first_seen|date:"j M G:i T" }}</b> | last <b>{{ issue.last_seen|date:"j M G:i T" }}</b> | with <b>{{ issue.digested_event_count|intcomma }}</b> events
|
||||
<div class="text-sm">from <b>{{ issue.first_seen|date:"j M G:i T" }}</b> | last <b>{{ issue.last_seen|date:"j M G:i T" }}</b> | {% blocktranslate with event_count=issue.digested_event_count|intcomma %}with <b>{{ event_count }}</b> events{% endblocktranslate %}
|
||||
{% if issue.digested_event_count != issue.stored_event_count %}
|
||||
<span class="text-xs">({{ issue.stored_event_count|intcomma }} av{#ilable#})<span>
|
||||
{% endif %}
|
||||
@@ -195,12 +196,12 @@
|
||||
No {{ state_filter }} issues found for "{{ q }}"
|
||||
{% else %}
|
||||
{% if state_filter == "open" %}
|
||||
Congratulations! You have no open issues.
|
||||
{% translate "Congratulations! You have no open issues." %}
|
||||
{% if project.digested_event_count == 0 %}
|
||||
This might mean you have not yet <a class="text-cyan-500 dark:text-cyan-300 font-bold" href="{% url "project_sdk_setup" project_pk=project.id %}">set up your SDK</a>.
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No {{ state_filter }} issues found.
|
||||
{% blocktranslate %}No {{ state_filter }} issues found.{% endblocktranslate %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -230,12 +231,12 @@
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.object_list|length > 0 %}{# sounds expensive, but this list is cached #}
|
||||
Issues {{ page_obj.start_index|intcomma }} – {{ page_obj.end_index|intcomma }}
|
||||
{% translate "Issues" %} {{ page_obj.start_index|intcomma }} – {{ page_obj.end_index|intcomma }}
|
||||
{% else %}
|
||||
{% if page_obj.number > 1 %}
|
||||
Less than {{ page_obj.start_index }} Issues {# corresponds to the 1/250 case of having an exactly full page and navigating to an empty page after that #}
|
||||
{% else %}
|
||||
0 Issues
|
||||
0 {% translate "Issues" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% load stricter_templates %}
|
||||
{% load issues %}
|
||||
{% load humanize %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block tab_content %}
|
||||
|
||||
@@ -21,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-6 mb-6 italic">
|
||||
No stacktrace available for this event.
|
||||
{% translate "No stacktrace available for this event." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "issues/base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block tab_content %}
|
||||
|
||||
@@ -20,7 +21,7 @@
|
||||
<h1 class="text-2xl font-bold mt-4">No tags</h1>
|
||||
|
||||
<div class="mb-6">
|
||||
No tags found for this issue.
|
||||
{% trans "No tags found for this issue." %}
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
BIN
locale/zh_Hans/LC_MESSAGES/django.mo
Normal file
BIN
locale/zh_Hans/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
891
locale/zh_Hans/LC_MESSAGES/django.po
Normal file
891
locale/zh_Hans/LC_MESSAGES/django.po
Normal file
@@ -0,0 +1,891 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-08-28 14:10+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"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: bsmain/templates/bsmain/auth_token_list.html:5
|
||||
#: bsmain/templates/bsmain/auth_token_list.html:23
|
||||
#: bsmain/templates/bsmain/auth_token_list.html:41
|
||||
msgid "Auth Tokens"
|
||||
msgstr "授权令牌"
|
||||
|
||||
#: bsmain/templates/bsmain/auth_token_list.html:28
|
||||
msgid "Add Token"
|
||||
msgstr "添加令牌"
|
||||
|
||||
#: bsmain/templates/bsmain/auth_token_list.html:54
|
||||
#: issues/templates/issues/issue_list.html:23
|
||||
#: issues/templates/issues/issue_list.html:147
|
||||
msgid "Delete"
|
||||
msgstr "删除"
|
||||
|
||||
#: bsmain/templates/bsmain/auth_token_list.html:63
|
||||
msgid "No Auth Tokens."
|
||||
msgstr "没有可用令牌。"
|
||||
|
||||
#: bsmain/views.py:23
|
||||
msgid "Token deleted"
|
||||
msgstr "令牌已删除"
|
||||
|
||||
#: issues/models.py:489 issues/templates/issues/base.html:201
|
||||
msgid "First seen"
|
||||
msgstr "首次出现"
|
||||
|
||||
#: issues/models.py:490 issues/templates/issues/base.html:165
|
||||
#: issues/templates/issues/issue_list.html:44
|
||||
msgid "Resolved"
|
||||
msgstr "已解决"
|
||||
|
||||
#: issues/models.py:491 issues/templates/issues/base.html:172
|
||||
#: issues/templates/issues/issue_list.html:43
|
||||
msgid "Muted"
|
||||
msgstr "已静默"
|
||||
|
||||
#: issues/models.py:492
|
||||
msgid "Marked as regressed"
|
||||
msgstr "标记为"
|
||||
|
||||
#: issues/models.py:493
|
||||
msgid "Unmuted"
|
||||
msgstr "已取消静默"
|
||||
|
||||
#: issues/models.py:495
|
||||
msgid "Release info added"
|
||||
msgstr ""
|
||||
|
||||
#: issues/models.py:498
|
||||
msgid "Manual annotation"
|
||||
msgstr "手动注释"
|
||||
|
||||
#: issues/templates/issues/_event_nav.html:5
|
||||
msgid "search..."
|
||||
msgstr "搜索..."
|
||||
|
||||
#: issues/templates/issues/base.html:23 issues/templates/issues/base.html:37
|
||||
#: issues/templates/issues/issue_list.html:79
|
||||
msgid "Resolved in next release"
|
||||
msgstr ""
|
||||
|
||||
#: issues/templates/issues/base.html:28 issues/templates/issues/base.html:56
|
||||
#: issues/templates/issues/issue_list.html:84
|
||||
#: issues/templates/issues/issue_list.html:109
|
||||
msgid "Resolve"
|
||||
msgstr "解决"
|
||||
|
||||
#: issues/templates/issues/base.html:64 issues/templates/issues/base.html:66
|
||||
#: issues/templates/issues/issue_list.html:117
|
||||
#: issues/templates/issues/issue_list.html:119
|
||||
msgid "Mute"
|
||||
msgstr "静默"
|
||||
|
||||
#: issues/templates/issues/base.html:71 issues/templates/issues/base.html:79
|
||||
#: issues/templates/issues/issue_list.html:124
|
||||
#: issues/templates/issues/issue_list.html:132
|
||||
msgid "Mute for/until "
|
||||
msgstr "静默至/ "
|
||||
|
||||
#: issues/templates/issues/base.html:88 issues/templates/issues/base.html:90
|
||||
#: issues/templates/issues/issue_list.html:138
|
||||
#: issues/templates/issues/issue_list.html:140
|
||||
msgid "Unmute"
|
||||
msgstr "取消静默"
|
||||
|
||||
#: issues/templates/issues/base.html:112
|
||||
msgid "Stacktrace"
|
||||
msgstr "栈追踪"
|
||||
|
||||
#: issues/templates/issues/base.html:113
|
||||
msgid "Event Details"
|
||||
msgstr "事件详情"
|
||||
|
||||
#: issues/templates/issues/base.html:114
|
||||
msgid "Breadcrumbs"
|
||||
msgstr "面包屑"
|
||||
|
||||
#: issues/templates/issues/base.html:115
|
||||
msgid "Event List"
|
||||
msgstr "事件列表"
|
||||
|
||||
#: issues/templates/issues/base.html:116
|
||||
msgid "Tags"
|
||||
msgstr "标签"
|
||||
|
||||
#: issues/templates/issues/base.html:117
|
||||
msgid "Grouping"
|
||||
msgstr "分组"
|
||||
|
||||
#: issues/templates/issues/base.html:118 issues/templates/issues/history.html:8
|
||||
#: templates/admin/change_form_object_tools.html:5
|
||||
msgid "History"
|
||||
msgstr "历史"
|
||||
|
||||
#: issues/templates/issues/base.html:127
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Event %(digest_order)s of %(total_events)s which occured at <span "
|
||||
"class=\"font-bold\">%(ingested_at)s</span>"
|
||||
msgstr ""
|
||||
"事件 %(digest_order)s 共 %(total_events)s 个 发生于 <span class=\"font-"
|
||||
"bold\">%(ingested_at)s</span>"
|
||||
|
||||
#: issues/templates/issues/base.html:130
|
||||
#: templates/admin/change_form_object_tools.html:8
|
||||
msgid "Download"
|
||||
msgstr "下载"
|
||||
|
||||
#: issues/templates/issues/base.html:131
|
||||
msgid "JSON"
|
||||
msgstr "JSON"
|
||||
|
||||
#: issues/templates/issues/base.html:132
|
||||
msgid "Plain"
|
||||
msgstr "原始文本"
|
||||
|
||||
#: issues/templates/issues/base.html:137
|
||||
msgid "Event Admin"
|
||||
msgstr "事件管理"
|
||||
|
||||
#: issues/templates/issues/base.html:139
|
||||
msgid "Issue Admin"
|
||||
msgstr "问题管理"
|
||||
|
||||
#: issues/templates/issues/base.html:157
|
||||
msgid "Issue"
|
||||
msgstr "问题"
|
||||
|
||||
#: issues/templates/issues/base.html:162
|
||||
msgid "State"
|
||||
msgstr "状态"
|
||||
|
||||
#: issues/templates/issues/base.html:167
|
||||
msgid "in"
|
||||
msgstr "于"
|
||||
|
||||
#: issues/templates/issues/base.html:183
|
||||
#: issues/templates/issues/issue_list.html:41
|
||||
msgid "Open"
|
||||
msgstr "打开"
|
||||
|
||||
#: issues/templates/issues/base.html:190
|
||||
msgid "Nr. of events"
|
||||
msgstr "事件数量"
|
||||
|
||||
#: issues/templates/issues/base.html:206
|
||||
msgid "Last seen"
|
||||
msgstr "最后出现"
|
||||
|
||||
#: issues/templates/issues/base.html:211
|
||||
msgid "Seen at"
|
||||
msgstr "出现时间"
|
||||
|
||||
#: issues/templates/issues/base.html:234
|
||||
msgid "Issue Tags"
|
||||
msgstr "问题标签"
|
||||
|
||||
#: issues/templates/issues/breadcrumbs.html:25
|
||||
msgid "No breadcrumbs available for this event."
|
||||
msgstr "这个事件没有可用的面包屑。"
|
||||
|
||||
#: issues/templates/issues/history.html:9
|
||||
msgid "Most recent first"
|
||||
msgstr "新内容优先"
|
||||
|
||||
#: issues/templates/issues/history.html:21
|
||||
msgid "Add comment as manual annotation"
|
||||
msgstr "添加评论作为手动注释"
|
||||
|
||||
#: issues/templates/issues/history.html:26
|
||||
msgid "Now"
|
||||
msgstr "现在"
|
||||
|
||||
#: issues/templates/issues/history.html:35
|
||||
msgid "comments..."
|
||||
msgstr "评论..."
|
||||
|
||||
#: issues/templates/issues/history.html:36
|
||||
msgid "Post comment"
|
||||
msgstr "发表评论"
|
||||
|
||||
#: issues/templates/issues/history.html:59
|
||||
msgctxt "History"
|
||||
msgid "by"
|
||||
msgstr "发表自"
|
||||
|
||||
#: issues/templates/issues/issue_list.html:7
|
||||
#: issues/templates/issues/issue_list.html:31
|
||||
#: issues/templates/issues/issue_list.html:234
|
||||
#: issues/templates/issues/issue_list.html:239 theme/templates/base.html:40
|
||||
msgid "Issues"
|
||||
msgstr "问题"
|
||||
|
||||
#: issues/templates/issues/issue_list.html:15
|
||||
msgid "Delete Issues"
|
||||
msgstr "删除问题"
|
||||
|
||||
#: issues/templates/issues/issue_list.html:18
|
||||
msgid ""
|
||||
"Deleting an Issue is a permanent action and cannot be undone. It's typically "
|
||||
"better to resolve or mute an issue instead of deleting it, as this allows "
|
||||
"you to keep track of past issues and their resolutions."
|
||||
msgstr ""
|
||||
"删除问题是一个无法被撤销的永久行为。通常更建议标记为解决或者静默,而不是直接"
|
||||
"删除问题,以便于追踪您过去的问题和决议。"
|
||||
|
||||
#: issues/templates/issues/issue_list.html:22
|
||||
#: projects/templates/projects/project_edit.html:21
|
||||
#: projects/templates/projects/project_edit.html:52
|
||||
#: projects/templates/projects/project_member_settings.html:43
|
||||
#: projects/templates/projects/project_member_settings.html:45
|
||||
#: projects/templates/projects/project_members_invite.html:52
|
||||
#: projects/templates/projects/project_new.html:27
|
||||
#: teams/templates/teams/team_edit.html:30
|
||||
#: teams/templates/teams/team_member_settings.html:43
|
||||
#: teams/templates/teams/team_member_settings.html:45
|
||||
#: teams/templates/teams/team_members_invite.html:54
|
||||
#: teams/templates/teams/team_new.html:25
|
||||
#: users/templates/users/user_edit.html:28
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
#: issues/templates/issues/issue_list.html:35
|
||||
msgid ""
|
||||
"The chosen action is not applicable to all selected issues. Issues for which "
|
||||
"it has not been applied have been left with checkboxes checked so that you "
|
||||
"can try again with another action."
|
||||
msgstr ""
|
||||
|
||||
#: issues/templates/issues/issue_list.html:42
|
||||
msgid "Unresolved"
|
||||
msgstr "未解决"
|
||||
|
||||
#: issues/templates/issues/issue_list.html:45
|
||||
msgid "All"
|
||||
msgstr "全部"
|
||||
|
||||
#: issues/templates/issues/issue_list.html:49
|
||||
msgid "Search issues..."
|
||||
msgstr "搜索问题..."
|
||||
|
||||
#: issues/templates/issues/issue_list.html:178
|
||||
#, python-format
|
||||
msgid "with <b>%(event_count)s</b> events"
|
||||
msgstr "共 <b>%(event_count)s</b> 个事件"
|
||||
|
||||
#: issues/templates/issues/issue_list.html:199
|
||||
msgid "Congratulations! You have no open issues."
|
||||
msgstr "恭喜你!现在没有打开的问题。"
|
||||
|
||||
#: issues/templates/issues/issue_list.html:204
|
||||
#, python-format
|
||||
msgid "No %(state_filter)s issues found."
|
||||
msgstr "没有找到 %(state_filter)s 的问题。"
|
||||
|
||||
#: issues/templates/issues/stacktrace.html:25
|
||||
msgid "No stacktrace available for this event."
|
||||
msgstr "这个事件没有栈追踪。"
|
||||
|
||||
#: issues/templates/issues/tags.html:24
|
||||
msgid "No tags found for this issue."
|
||||
msgstr "这个问题没有标签。"
|
||||
|
||||
#: projects/forms.py:17 teams/forms.py:14 users/forms.py:87
|
||||
msgid "Email"
|
||||
msgstr "电子邮件"
|
||||
|
||||
#: projects/forms.py:19 projects/forms.py:48 teams/forms.py:16
|
||||
#: teams/forms.py:46
|
||||
msgid "Role"
|
||||
msgstr "角色"
|
||||
|
||||
#: projects/forms.py:67
|
||||
#, python-format
|
||||
msgid "Default (%s, as per %s settings)"
|
||||
msgstr ""
|
||||
|
||||
#: projects/forms.py:68 teams/forms.py:55 users/forms.py:151
|
||||
msgid "Send email alerts"
|
||||
msgstr "发送邮件通知"
|
||||
|
||||
#: projects/forms.py:89
|
||||
msgctxt "Project"
|
||||
msgid "Name"
|
||||
msgstr "项目名称"
|
||||
|
||||
#: projects/forms.py:90 teams/forms.py:76
|
||||
msgid "Visibility"
|
||||
msgstr "可见性"
|
||||
|
||||
#: projects/forms.py:92
|
||||
msgid "Retention max event count"
|
||||
msgstr "事件保留上限"
|
||||
|
||||
#: projects/forms.py:93
|
||||
msgid "The maximum number of events to store before evicting."
|
||||
msgstr "超过此数量的事件将会被丢弃。"
|
||||
|
||||
#: projects/forms.py:101
|
||||
msgid "DSN (read-only)"
|
||||
msgstr "DSN (只读)"
|
||||
|
||||
#. Translators: {link} will be replaced with an HTML link; place it where it reads naturally.
|
||||
#, python-brace-format
|
||||
msgid "Use the DSN to {link}."
|
||||
msgstr "使用 DSN 来{link}。"
|
||||
|
||||
#: projects/forms.py:103
|
||||
msgid "set up the SDK"
|
||||
msgstr "设置 SDK"
|
||||
|
||||
#: projects/forms.py:115
|
||||
#. Translators: This text is followed by a clickable link. Adjust punctuation/spacing naturally.
|
||||
msgid "You don't have any teams yet; "
|
||||
msgstr "你还没有任何团队;"
|
||||
|
||||
#: projects/forms.py:115
|
||||
msgid "Create a team first."
|
||||
msgstr "先创建一个团队。"
|
||||
|
||||
#: projects/models.py:50 teams/models.py:9
|
||||
msgid "Member"
|
||||
msgstr "成员"
|
||||
|
||||
#: projects/models.py:51 projects/templates/projects/project_list.html:87
|
||||
#: projects/templates/projects/project_members.html:51 teams/models.py:10
|
||||
#: teams/templates/teams/team_list.html:70
|
||||
#: teams/templates/teams/team_members.html:51 theme/templates/base.html:45
|
||||
msgid "Admin"
|
||||
msgstr "管理员"
|
||||
|
||||
#: projects/models.py:56 teams/models.py:15
|
||||
msgid "Joinable"
|
||||
msgstr "可加入"
|
||||
|
||||
#: projects/models.py:60 teams/models.py:20
|
||||
msgid "Discoverable"
|
||||
msgstr "可发现"
|
||||
|
||||
#: projects/models.py:64 teams/templates/teams/team_members.html:25
|
||||
msgid "Team Members"
|
||||
msgstr "团队成员"
|
||||
|
||||
#: projects/models.py:113
|
||||
msgid "Which users can see this project and its issues?"
|
||||
msgstr "哪些用户可以看到这个项目以及它们的问题?"
|
||||
|
||||
#: projects/templates/projects/project_alerts_setup.html:5
|
||||
#: projects/templates/projects/project_alerts_setup.html:25
|
||||
msgid "Alerts"
|
||||
msgstr "通知"
|
||||
|
||||
#: projects/templates/projects/project_alerts_setup.html:28
|
||||
msgid "Add"
|
||||
msgstr "添加"
|
||||
|
||||
#: projects/templates/projects/project_alerts_setup.html:40
|
||||
msgid "Messaging Services"
|
||||
msgstr "消息服务"
|
||||
|
||||
#: projects/templates/projects/project_alerts_setup.html:68
|
||||
msgctxt "Alerts"
|
||||
msgid "Test"
|
||||
msgstr "测试"
|
||||
|
||||
#: projects/templates/projects/project_alerts_setup.html:69
|
||||
msgctxt "Alerts"
|
||||
msgid "Remove"
|
||||
msgstr "移除"
|
||||
|
||||
#: projects/templates/projects/project_alerts_setup.html:78
|
||||
msgid "No Messaging Services Configured."
|
||||
msgstr "没有已配置的消息通知服务。"
|
||||
|
||||
#: projects/templates/projects/project_alerts_setup.html:78
|
||||
msgid "Add Messaging Service"
|
||||
msgstr "添加消息服务"
|
||||
|
||||
#: projects/templates/projects/project_alerts_setup.html:92
|
||||
msgctxt "Alerts"
|
||||
msgid "Settings"
|
||||
msgstr "项目设置"
|
||||
|
||||
#: projects/templates/projects/project_alerts_setup.html:93
|
||||
#: projects/templates/projects/project_members.html:90
|
||||
msgid "Back to Projects"
|
||||
msgstr "返回项目列表"
|
||||
|
||||
#: projects/templates/projects/project_edit.html:6
|
||||
#: teams/templates/teams/team_edit.html:6
|
||||
#: users/templates/users/user_edit.html:6
|
||||
msgid "Edit"
|
||||
msgstr "编辑"
|
||||
|
||||
#: projects/templates/projects/project_edit.html:14
|
||||
#: projects/templates/projects/project_edit.html:53
|
||||
msgid "Delete Project"
|
||||
msgstr "删除项目"
|
||||
|
||||
#: projects/templates/projects/project_edit.html:17
|
||||
msgid ""
|
||||
"Are you sure you want to delete this project? This action cannot be undone "
|
||||
"and will delete all associated data."
|
||||
msgstr "确定删除该项目?该操作不可撤销。"
|
||||
|
||||
#: projects/templates/projects/project_edit.html:38
|
||||
#, python-format
|
||||
msgid "Settings (%(project_name)s)"
|
||||
msgstr "项目设置 (%(project_name)s)"
|
||||
|
||||
#: projects/templates/projects/project_edit.html:42
|
||||
#, python-format
|
||||
msgid "Project settings for \"%(project_name)s\"."
|
||||
msgstr "项目 \"%(project_name)s\" 的设置。"
|
||||
|
||||
#: projects/templates/projects/project_edit.html:51
|
||||
#: projects/templates/projects/project_member_settings.html:41
|
||||
#: projects/templates/projects/project_new.html:26
|
||||
#: teams/templates/teams/team_edit.html:29
|
||||
#: teams/templates/teams/team_member_settings.html:41
|
||||
#: teams/templates/teams/team_new.html:24
|
||||
#: users/templates/users/preferences.html:34
|
||||
#: users/templates/users/user_edit.html:27
|
||||
msgid "Save"
|
||||
msgstr "保存"
|
||||
|
||||
#: projects/templates/projects/project_list.html:5
|
||||
#: projects/templates/projects/project_list.html:14
|
||||
#: theme/templates/base.html:37
|
||||
msgid "Projects"
|
||||
msgstr "项目"
|
||||
|
||||
#: projects/templates/projects/project_list.html:21
|
||||
#: projects/templates/projects/project_new.html:6
|
||||
#: projects/templates/projects/project_new.html:18
|
||||
msgid "New Project"
|
||||
msgstr "创建新项目"
|
||||
|
||||
#: projects/templates/projects/project_list.html:42
|
||||
msgid "My Projects"
|
||||
msgstr "我的项目"
|
||||
|
||||
#: projects/templates/projects/project_list.html:44
|
||||
msgid "Team Projects"
|
||||
msgstr "团队项目"
|
||||
|
||||
#: projects/templates/projects/project_list.html:45
|
||||
msgid "Other Projects"
|
||||
msgstr "其他项目"
|
||||
|
||||
#: projects/templates/projects/project_list.html:75
|
||||
#: teams/templates/teams/team_list.html:58
|
||||
msgid "members"
|
||||
msgstr "个成员"
|
||||
|
||||
#: projects/templates/projects/project_list.html:78
|
||||
#: teams/templates/teams/team_list.html:60
|
||||
msgid "my settings"
|
||||
msgstr "我的设置"
|
||||
|
||||
#: projects/templates/projects/project_list.html:85
|
||||
msgid "You're invited!"
|
||||
msgstr "您收到了邀请!"
|
||||
|
||||
#: projects/templates/projects/project_list.html:155
|
||||
#: teams/templates/teams/team_list.html:105
|
||||
msgid "Invitation"
|
||||
msgstr "接受邀请"
|
||||
|
||||
#: projects/templates/projects/project_list.html:159
|
||||
#: projects/templates/projects/project_members.html:62
|
||||
#: teams/templates/teams/team_list.html:109
|
||||
#: teams/templates/teams/team_members.html:62
|
||||
msgid "Leave"
|
||||
msgstr "离开"
|
||||
|
||||
#: projects/templates/projects/project_list.html:165
|
||||
#: teams/templates/teams/team_list.html:115
|
||||
msgid "Join"
|
||||
msgstr "加入"
|
||||
|
||||
#: projects/templates/projects/project_list.html:175
|
||||
msgid "No projects found."
|
||||
msgstr "没有找到项目。"
|
||||
|
||||
#: projects/templates/projects/project_member_settings.html:6
|
||||
msgid "Member settings"
|
||||
msgstr "成员设置"
|
||||
|
||||
#: projects/templates/projects/project_member_settings.html:27
|
||||
#: teams/templates/teams/team_member_settings.html:27
|
||||
msgid "Membership settings"
|
||||
msgstr "成员设置"
|
||||
|
||||
#: projects/templates/projects/project_member_settings.html:32
|
||||
#, python-format
|
||||
msgid "Your membership settings for project \"%(project_name)s\"."
|
||||
msgstr "你作为项目 \"%(project_name)s\" 的成员设置。"
|
||||
|
||||
#: projects/templates/projects/project_member_settings.html:34
|
||||
#, python-format
|
||||
msgid "Settings for project \"%(project_name)s\" and user %(username)s."
|
||||
msgstr "用户 %(username)s 作为项目 \"%(project_name)s\" 的成员设置。"
|
||||
|
||||
#: projects/templates/projects/project_members.html:25
|
||||
msgid "Project Members"
|
||||
msgstr "项目成员"
|
||||
|
||||
#: projects/templates/projects/project_members.html:28
|
||||
#: projects/templates/projects/project_members_invite.html:50
|
||||
#: teams/templates/teams/team_members.html:28
|
||||
#: teams/templates/teams/team_members_invite.html:52
|
||||
msgid "Invite Member"
|
||||
msgstr "邀请成员"
|
||||
|
||||
#: projects/templates/projects/project_members.html:49
|
||||
#: teams/templates/teams/team_members.html:49
|
||||
msgid "Invitation pending"
|
||||
msgstr "等待同意邀请"
|
||||
|
||||
#: projects/templates/projects/project_members.html:59
|
||||
#: teams/templates/teams/team_members.html:59
|
||||
msgid "Reinvite"
|
||||
msgstr "重新邀请"
|
||||
|
||||
#: projects/templates/projects/project_members.html:64
|
||||
#: teams/templates/teams/team_members.html:64
|
||||
msgid "Remove"
|
||||
msgstr "移除团队"
|
||||
|
||||
#: projects/templates/projects/project_members.html:75
|
||||
#: teams/templates/teams/team_members.html:75
|
||||
msgid "No members yet."
|
||||
msgstr "当前还没有成员。"
|
||||
|
||||
#: projects/templates/projects/project_members.html:75
|
||||
#: teams/templates/teams/team_members.html:75
|
||||
msgid "Invite someone</a>."
|
||||
msgstr "邀请成员</a>。"
|
||||
|
||||
#: projects/templates/projects/project_members.html:89
|
||||
#, python-format
|
||||
msgid " %(team_name)s Members"
|
||||
msgstr "%(team_name)s 团队的成员"
|
||||
|
||||
#: projects/templates/projects/project_members_accept.html:18
|
||||
msgid "Invitation to"
|
||||
msgstr "受邀加入"
|
||||
|
||||
#: projects/templates/projects/project_members_accept.html:22
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You have been invited to join the project \"%(project_name)s\" in the role "
|
||||
"of \"%(role)s\". Please confirm by clicking the button below."
|
||||
msgstr ""
|
||||
"您正在作为 \"%(role)s\" 被邀请加入项目 \"%(project_name)s\" 项目的角色。请单"
|
||||
"击下面的按钮确认。"
|
||||
|
||||
#: projects/templates/projects/project_members_accept.html:25
|
||||
msgid "Accept"
|
||||
msgstr "接受"
|
||||
|
||||
#: projects/templates/projects/project_members_accept.html:26
|
||||
msgid "Decline"
|
||||
msgstr "拒绝"
|
||||
|
||||
#: projects/templates/projects/project_members_invite.html:26
|
||||
#, python-format
|
||||
msgid "Invite members (%(project_name)s)"
|
||||
msgstr "邀请成员 (%(project_name)s)"
|
||||
|
||||
#: projects/templates/projects/project_members_invite.html:30
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Invite a member to join the project \"%(project_name)s\". They will receive "
|
||||
"an email with a link to join."
|
||||
msgstr ""
|
||||
"邀请一名新的成员加入项目 \"%(project_name)s\"。对方将收到一封含有加入团队链接"
|
||||
"的电子邮件。"
|
||||
|
||||
#: projects/templates/projects/project_members_invite.html:51
|
||||
#: teams/templates/teams/team_members_invite.html:53
|
||||
msgid "Invite and add another"
|
||||
msgstr "邀请并继续添加"
|
||||
|
||||
#: projects/views.py:82
|
||||
#, python-format
|
||||
msgid "You have joined the project \"%s\""
|
||||
msgstr "你已经加入了项目 \"%s\""
|
||||
|
||||
#: teams/forms.py:54
|
||||
#, python-format
|
||||
msgid "User-default (%s)"
|
||||
msgstr "用户缺省 (%s)"
|
||||
|
||||
#: teams/forms.py:75
|
||||
msgctxt "Team"
|
||||
msgid "Name"
|
||||
msgstr "团队名称"
|
||||
|
||||
#: teams/models.py:23
|
||||
msgid "Hidden"
|
||||
msgstr ""
|
||||
|
||||
#: teams/models.py:33
|
||||
msgid "Which users can see this team and its issues?"
|
||||
msgstr "哪些用户可以看到这个团队以及它们的问题?"
|
||||
|
||||
#: teams/templates/teams/team_edit.html:18
|
||||
#, python-format
|
||||
msgid "Settings (%(team_name)s)"
|
||||
msgstr "团队设置 (%(team_name)s)"
|
||||
|
||||
#: teams/templates/teams/team_edit.html:22
|
||||
#, python-format
|
||||
msgid "Team settings for \"%(team_name)s\"."
|
||||
msgstr "团队 \"%(team_name)s\" 的设置。"
|
||||
|
||||
#: teams/templates/teams/team_list.html:5
|
||||
#: teams/templates/teams/team_list.html:14 theme/templates/base.html:34
|
||||
msgid "Teams"
|
||||
msgstr "团队"
|
||||
|
||||
#: teams/templates/teams/team_list.html:21
|
||||
#: teams/templates/teams/team_new.html:18
|
||||
msgid "New Team"
|
||||
msgstr "创建新团队"
|
||||
|
||||
#: teams/templates/teams/team_list.html:32
|
||||
msgid "My Teams"
|
||||
msgstr "我的团队"
|
||||
|
||||
#: teams/templates/teams/team_list.html:33
|
||||
msgid "Other Teams"
|
||||
msgstr "其他团队"
|
||||
|
||||
#: teams/templates/teams/team_list.html:57
|
||||
msgid "projects"
|
||||
msgstr "个项目"
|
||||
|
||||
#: teams/templates/teams/team_list.html:125
|
||||
msgid "No teams found."
|
||||
msgstr "没有找到团队。"
|
||||
|
||||
#: teams/templates/teams/team_member_settings.html:32
|
||||
#, python-format
|
||||
msgid "Your membership settings for team \"%(team_name)s\"."
|
||||
msgstr "你作为团队 \"%(team_name)s\" 的成员设置。"
|
||||
|
||||
#: teams/templates/teams/team_member_settings.html:34
|
||||
#, python-format
|
||||
msgid "Settings for team \"%(team_name)s\" and user %(username)s."
|
||||
msgstr "用户 %(username)s 作为团队 \"%(team_name)s\" 的成员设置。"
|
||||
|
||||
#: teams/templates/teams/team_members.html:5
|
||||
msgid "Members"
|
||||
msgstr "成员"
|
||||
|
||||
#: teams/templates/teams/team_members.html:89
|
||||
msgid "Back to Teams"
|
||||
msgstr "返回团队列表"
|
||||
|
||||
#: teams/templates/teams/team_members_invite.html:28
|
||||
#, python-format
|
||||
msgid "Invite members (%(team_name)s)"
|
||||
msgstr "邀请成员 (%(team_name)s)"
|
||||
|
||||
#: teams/templates/teams/team_members_invite.html:32
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Invite a member to join the team \"%(team_name)s\". They will receive an "
|
||||
"email with a link to join."
|
||||
msgstr ""
|
||||
"邀请一名新的成员加入团队 \"%(team_name)s\"。对方将收到一封含有加入团队链接的"
|
||||
"电子邮件。"
|
||||
|
||||
#: teams/views.py:58
|
||||
#, python-format
|
||||
msgid "You have joined the team \"%s\""
|
||||
msgstr "你已经加入了团队 \"%s\""
|
||||
|
||||
#: templates/admin/change_form_object_tools.html:7
|
||||
msgid "Raw"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin/change_form_object_tools.html:9
|
||||
msgid "View on site"
|
||||
msgstr ""
|
||||
|
||||
#: templates/bugsink/login.html:18
|
||||
msgid "Your username and password didn't match. Please try again."
|
||||
msgstr "用户名和密码不匹配,请重试"
|
||||
|
||||
#: templates/bugsink/login.html:22
|
||||
msgid ""
|
||||
"Your account doesn't have access to this page. To proceed, please login with "
|
||||
"an account that has access."
|
||||
msgstr "您的帐户没有权限访问此页面,请登录具有权限的账户后重试"
|
||||
|
||||
#: templates/bugsink/login.html:24
|
||||
msgid "Please login to see this page."
|
||||
msgstr "请登录后查看此页面"
|
||||
|
||||
#: templates/bugsink/login.html:34
|
||||
msgid "Username"
|
||||
msgstr "用户名"
|
||||
|
||||
#: templates/bugsink/login.html:40
|
||||
msgid "Password"
|
||||
msgstr "密码"
|
||||
|
||||
#: templates/bugsink/login.html:43 theme/templates/base.html:54
|
||||
msgid "Login"
|
||||
msgstr "登录"
|
||||
|
||||
#: templates/bugsink/login.html:48
|
||||
msgid "Forgot password?"
|
||||
msgstr "忘记密码?"
|
||||
|
||||
#: templates/bugsink/login.html:49
|
||||
msgid "Create an account"
|
||||
msgstr "创建一个新账户"
|
||||
|
||||
#: templates/bugsink/settings.html:7
|
||||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
#: templates/bugsink/settings.html:14
|
||||
msgid "Site Settings"
|
||||
msgstr "站点设置"
|
||||
|
||||
#: theme/templates/base.html:49 users/templates/users/user_list.html:45
|
||||
#: users/templates/users/user_list.html:63
|
||||
msgid "Users"
|
||||
msgstr "用户"
|
||||
|
||||
#: theme/templates/base.html:50
|
||||
msgid "Tokens"
|
||||
msgstr "令牌"
|
||||
|
||||
#: theme/templates/base.html:56
|
||||
msgid "Preferences"
|
||||
msgstr "偏好"
|
||||
|
||||
#: theme/templates/base.html:57
|
||||
msgid "Log out"
|
||||
msgstr "登出"
|
||||
|
||||
#: users/forms.py:17
|
||||
msgid "Yes"
|
||||
msgstr "是"
|
||||
|
||||
#: users/forms.py:18
|
||||
msgid "No"
|
||||
msgstr "否"
|
||||
|
||||
#: users/forms.py:137
|
||||
msgid "Auto (browser preference)"
|
||||
msgstr "自动(浏览器偏好)"
|
||||
|
||||
#: users/forms.py:153
|
||||
msgid "Theme preference"
|
||||
msgstr "主题偏好"
|
||||
|
||||
#: users/forms.py:159
|
||||
msgid "Language"
|
||||
msgstr "语言"
|
||||
|
||||
#: users/models.py:21
|
||||
msgid "System Default"
|
||||
msgstr "系统缺省"
|
||||
|
||||
#: users/models.py:22
|
||||
msgid "Light"
|
||||
msgstr "明亮"
|
||||
|
||||
#: users/models.py:23
|
||||
msgid "Dark"
|
||||
msgstr "黑暗"
|
||||
|
||||
#: users/templates/users/confirm_email.html:18
|
||||
msgid "Confirm your email address by clicking the button below."
|
||||
msgstr ""
|
||||
|
||||
#: users/templates/users/confirm_email.html:23
|
||||
msgid "Confirm"
|
||||
msgstr "确认"
|
||||
|
||||
#: users/templates/users/confirm_email_sent.html:17
|
||||
msgid ""
|
||||
"A verification email has been sent to your email address. Please verify your "
|
||||
"email address to complete the registration process."
|
||||
msgstr ""
|
||||
|
||||
#: users/templates/users/logged_out.html:16
|
||||
msgid "You have been logged out."
|
||||
msgstr "你已经从系统中登出。"
|
||||
|
||||
#: users/templates/users/logged_out.html:16
|
||||
msgid "Log in again</a>."
|
||||
msgstr "重新登录</a>。"
|
||||
|
||||
#: users/templates/users/preferences.html:27
|
||||
msgid "User Preferences"
|
||||
msgstr "用户偏好"
|
||||
|
||||
#: users/templates/users/request_reset_password.html:23
|
||||
#: users/templates/users/reset_password.html:27
|
||||
msgid "Reset password"
|
||||
msgstr "重置密码"
|
||||
|
||||
#: users/templates/users/request_reset_password.html:27
|
||||
#: users/templates/users/reset_password.html:32
|
||||
msgid "Log in instead"
|
||||
msgstr ""
|
||||
|
||||
#: users/templates/users/resend_confirmation.html:23
|
||||
msgid "Resend verification email"
|
||||
msgstr ""
|
||||
|
||||
#: users/templates/users/reset_password_email_sent.html:16
|
||||
msgid ""
|
||||
"A password reset link has been sent to your email address. Please check your "
|
||||
"inbox and follow the instructions to reset your password."
|
||||
msgstr ""
|
||||
"一封含有密码重置链接的电子邮件已经发送给您。请您检查您的收件箱并按说明重置密"
|
||||
"码。"
|
||||
|
||||
#: users/templates/users/user_edit.html:22
|
||||
#, python-format
|
||||
msgid "Settings for %(username)s."
|
||||
msgstr "%(username)s 的设置。"
|
||||
|
||||
#: users/templates/users/user_list.html:16
|
||||
msgid ""
|
||||
"Are you sure you want to delete this user? This action cannot be undone."
|
||||
msgstr "确定删除该用户?该操作不可撤销。"
|
||||
|
||||
#: users/templates/users/user_list.html:72
|
||||
msgid "Superuser"
|
||||
msgstr "超级管理员"
|
||||
|
||||
#: users/views.py:237
|
||||
msgid "Updated preferences"
|
||||
msgstr "偏好已更新"
|
||||
|
||||
# Fix on https://code.djangoproject.com/ticket/36579
|
||||
msgid "yes,no,maybe"
|
||||
msgstr "是,否,也许"
|
||||
@@ -2,6 +2,8 @@ from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.template.defaultfilters import yesno
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.html import format_html
|
||||
|
||||
from bugsink.utils import assert_
|
||||
from teams.models import TeamMembership
|
||||
@@ -12,9 +14,10 @@ User = get_user_model()
|
||||
|
||||
|
||||
class ProjectMemberInviteForm(forms.Form):
|
||||
email = forms.EmailField(label='Email', required=True)
|
||||
email = forms.EmailField(label=_('Email'), required=True)
|
||||
role = forms.ChoiceField(
|
||||
label='Role', choices=ProjectRole.choices, required=True, initial=ProjectRole.MEMBER, widget=forms.RadioSelect)
|
||||
label=_('Role'), choices=ProjectRole.choices, required=True, initial=ProjectRole.MEMBER,
|
||||
widget=forms.RadioSelect)
|
||||
|
||||
def __init__(self, user_must_exist, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -59,7 +62,7 @@ class MyProjectMembershipForm(forms.ModelForm):
|
||||
sea_defined_at = "user"
|
||||
sea_default = self.instance.user.send_email_alerts
|
||||
|
||||
empty_label = 'Default (%s, as per %s settings)' % (yesno(sea_default).capitalize(), sea_defined_at)
|
||||
empty_label = _('Default (%s, as per %s settings)') % (yesno(sea_default).capitalize(), sea_defined_at)
|
||||
self.fields['send_email_alerts'].empty_label = empty_label
|
||||
self.fields['send_email_alerts'].widget.choices[0] = ("unknown", empty_label)
|
||||
|
||||
@@ -80,7 +83,7 @@ class ProjectForm(forms.ModelForm):
|
||||
team_qs = kwargs.pop("team_qs", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields["retention_max_event_count"].help_text = "The maximum number of events to store before evicting."
|
||||
self.fields["retention_max_event_count"].help_text = _("The maximum number of events to store before evicting.")
|
||||
if self.instance is not None and self.instance.pk is not None:
|
||||
# for editing, we disallow changing the team. consideration: it's somewhat hard to see what the consequences
|
||||
# for authorization are (from the user's perspective).
|
||||
@@ -88,10 +91,13 @@ class ProjectForm(forms.ModelForm):
|
||||
|
||||
# for editing, the DSN is availabe, but read-only
|
||||
self.fields["dsn"].initial = self.instance.dsn
|
||||
self.fields["dsn"].label = "DSN (read-only)"
|
||||
self.fields["dsn"].help_text = 'Use the DSN to <a href="' +\
|
||||
reverse('project_sdk_setup', kwargs={'project_pk': self.instance.pk}) +\
|
||||
'" class="text-cyan-800 font-bold">set up the SDK</a>.'
|
||||
self.fields["dsn"].label = _("DSN (read-only)")
|
||||
href = reverse('project_sdk_setup', kwargs={'project_pk': self.instance.pk})
|
||||
|
||||
self.fields["dsn"].help_text = format_html(
|
||||
_("Use the DSN to {link}."),
|
||||
link=format_html('<a href="{}" class="text-cyan-800 font-bold">{}</a>', href, _("set up the SDK")),
|
||||
)
|
||||
|
||||
# if we ever push slug to the form, editing it should probably be disallowed as well (but mainly because it
|
||||
# has consequences on the issue's short identifier)
|
||||
@@ -102,9 +108,11 @@ class ProjectForm(forms.ModelForm):
|
||||
# it suggests at least somewhere that teams are a thing)
|
||||
self.fields["team"].queryset = team_qs
|
||||
if team_qs.count() == 0:
|
||||
self.fields["team"].help_text = 'You don\'t have any teams yet; <a href="' +\
|
||||
reverse("team_new") +\
|
||||
'" class="text-cyan-800 font-bold">Create a team first.</a>'
|
||||
href = reverse("team_new")
|
||||
self.fields["team"].help_text = format_html(
|
||||
"{}{}", _("You don't have any teams yet; "),
|
||||
format_html('<a href="{}" class="text-cyan-800 font-bold">{}</a>', href, _("Create a team first.")))
|
||||
|
||||
elif team_qs.count() == 1:
|
||||
self.fields["team"].initial = team_qs.first()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import uuid
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from bugsink.app_settings import get_settings
|
||||
from bugsink.transaction import delay_on_commit
|
||||
@@ -46,21 +47,21 @@ from .tasks import delete_project_deps
|
||||
|
||||
|
||||
class ProjectRole(models.IntegerChoices):
|
||||
MEMBER = 0
|
||||
ADMIN = 1
|
||||
MEMBER = 0, _("Member")
|
||||
ADMIN = 1, _("Admin")
|
||||
|
||||
|
||||
class ProjectVisibility(models.IntegerChoices):
|
||||
# PUBLIC = 0 # anyone can see the project and its members; not sure if I want this or always require click-in
|
||||
JOINABLE = 1 # anyone can join
|
||||
JOINABLE = 1, _("Joinable") # anyone can join
|
||||
|
||||
# the project's existance is visible, but the project itself is not. the idea would be that you can "request to
|
||||
# join" (which is currently not implemented as a button, but you could do it 'out of bands' i.e. via email or chat).
|
||||
DISCOVERABLE = 10
|
||||
DISCOVERABLE = 10, _("Discoverable")
|
||||
|
||||
# the project's exsitance is only visible to team-members; you still need to explicitly click "join" which will
|
||||
# immediately make you a member
|
||||
TEAM_MEMBERS = 99
|
||||
TEAM_MEMBERS = 99, _("Team Members")
|
||||
|
||||
# having projects that are part of a certain team, but not visible to the team members, was considered, but
|
||||
# rejected. The basic thinking on the rejection is: it would hollow out the concept of Team to the point of
|
||||
@@ -75,7 +76,7 @@ class Project(models.Model):
|
||||
|
||||
team = models.ForeignKey("teams.Team", blank=False, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
name = models.CharField(max_length=255, blank=False, null=False, unique=True)
|
||||
name = models.CharField(pgettext_lazy("Project", "Name"), max_length=255, blank=False, null=False, unique=True)
|
||||
slug = models.SlugField(max_length=50, blank=False, null=False, unique=True)
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
|
||||
@@ -108,15 +109,15 @@ class Project(models.Model):
|
||||
|
||||
# visibility
|
||||
visibility = models.IntegerField(
|
||||
choices=ProjectVisibility.choices, default=ProjectVisibility.TEAM_MEMBERS,
|
||||
help_text="Which users can see this project and its issues?")
|
||||
_("Visibility"), choices=ProjectVisibility.choices, default=ProjectVisibility.TEAM_MEMBERS,
|
||||
help_text=_("Which users can see this project and its issues?"))
|
||||
|
||||
# ingestion/digestion quota
|
||||
quota_exceeded_until = models.DateTimeField(null=True, blank=True)
|
||||
next_quota_check = models.PositiveIntegerField(null=False, default=0)
|
||||
|
||||
# retention
|
||||
retention_max_event_count = models.PositiveIntegerField(default=10_000)
|
||||
retention_max_event_count = models.PositiveIntegerField(_("Retention max event count"), default=10_000)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -178,9 +179,9 @@ class ProjectMembership(models.Model):
|
||||
project = models.ForeignKey(Project, on_delete=models.DO_NOTHING)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
|
||||
send_email_alerts = models.BooleanField(default=None, null=True)
|
||||
send_email_alerts = models.BooleanField(_("Send email alerts"), default=None, null=True)
|
||||
|
||||
role = models.IntegerField(choices=ProjectRole.choices, default=ProjectRole.MEMBER)
|
||||
role = models.IntegerField(_("Role"), choices=ProjectRole.choices, default=ProjectRole.MEMBER)
|
||||
accepted = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Alerts · {{ project.name }} · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "Alerts" %} · {{ project.name }} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -21,10 +22,10 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="flex">
|
||||
<h1 class="text-4xl mt-4 font-bold">{{ project.name }} · Alerts</h1>
|
||||
<h1 class="text-4xl mt-4 font-bold">{{ project.name }} · {% translate "Alerts" %}</h1>
|
||||
|
||||
<div class="ml-auto mt-6">
|
||||
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "project_messaging_service_add" project_pk=project.pk %}">Add</a>
|
||||
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "project_messaging_service_add" project_pk=project.pk %}">{% translate "Add" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +37,7 @@
|
||||
<tbody>
|
||||
<thead>
|
||||
<tr class="bg-slate-200 dark:bg-slate-800">
|
||||
<th class="w-full p-4 text-left text-xl" colspan="2">Messaging Services</th>
|
||||
<th class="w-full p-4 text-left text-xl" colspan="2">{% translate "Messaging Services" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for service_config in service_configs %}
|
||||
@@ -64,8 +65,8 @@
|
||||
|
||||
<td class="p-4">
|
||||
<div class="flex justify-end">
|
||||
<button name="action" value="test:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Test</button>
|
||||
<button name="action" value="remove:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Remove</button>
|
||||
<button name="action" value="test:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Test" context "Alerts" %}</button>
|
||||
<button name="action" value="remove:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Remove" context "Alerts" %}</button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -74,7 +75,7 @@
|
||||
<tr class="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 border-b-2">
|
||||
<td class="w-full p-4">
|
||||
<div>
|
||||
No Messaging Services Configured. <a href="{% url "project_messaging_service_add" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Add Messaging Service</a>.
|
||||
{% translate "No Messaging Services Configured." %} <a href="{% url "project_messaging_service_add" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Add Messaging Service" %}</a>.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -88,8 +89,8 @@
|
||||
|
||||
<div class="flex flex-direction-row">
|
||||
<div class="ml-auto py-8 pr-4">
|
||||
<a href="{% url "project_edit" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Settings</a>
|
||||
<span class="font-bold text-slate-500 dark:text-slate-300">|</span> <a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Back to Projects</a>
|
||||
<a href="{% url "project_edit" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Settings" context "Alerts" %}</a>
|
||||
<span class="font-bold text-slate-500 dark:text-slate-300">|</span> <a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Back to Projects" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Edit {{ project.name }} · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "Edit" %} {{ project.name }} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -10,14 +11,14 @@
|
||||
<div id="deleteModal" class="hidden fixed inset-0 bg-slate-600 dark:bg-slate-900 bg-opacity-50 dark:bg-opacity-50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
|
||||
<div class="relative p-6 border border-slate-300 dark:border-slate-600 w-96 shadow-lg rounded-md bg-white dark:bg-slate-900">
|
||||
<div class="text-center m-4">
|
||||
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">Delete Project</h3>
|
||||
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">{% translate "Delete Project" %}</h3>
|
||||
<div class="mt-4 mb-6">
|
||||
<p class="text-slate-700 dark:text-slate-300">
|
||||
Are you sure you want to delete this project? This action cannot be undone and will delete all associated data.
|
||||
{% translate "Are you sure you want to delete this project? This action cannot be undone and will delete all associated data." %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-center space-x-4 mb-4">
|
||||
<button id="cancelDelete" class="text-cyan-500 dark:text-cyan-300 font-bold">Cancel</button>
|
||||
<button id="cancelDelete" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Cancel" %}</button>
|
||||
<form method="post" action="." id="deleteForm">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="delete">
|
||||
@@ -34,11 +35,11 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl my-4 font-bold">Settings ({{ project.name }})</h1>
|
||||
<h1 class="text-4xl my-4 font-bold">{% blocktranslate with project_name=project.name %}Settings ({{ project_name }}){% endblocktranslate %}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-4">
|
||||
Project settings for "{{ project.name }}".
|
||||
{% blocktranslate with project_name=project.name %}Project settings for "{{ project_name }}".{% endblocktranslate %}
|
||||
</div>
|
||||
|
||||
{% tailwind_formfield form.name %}
|
||||
@@ -47,9 +48,9 @@
|
||||
{% tailwind_formfield form.dsn %}
|
||||
|
||||
<div class="flex items-center mt-4">
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
|
||||
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
|
||||
<button type="button" id="deleteButton" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring ml-4 ml-auto">Delete Project</button>
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
|
||||
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
|
||||
<button type="button" id="deleteButton" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring ml-4 ml-auto">{% translate "Delete Project" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Projects · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "Projects" %} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -10,14 +11,14 @@
|
||||
<div class="m-4 flex flex-row items-end">
|
||||
|
||||
<div><!-- top, LHS (h1) -->
|
||||
<h1 class="text-4xl mt-4 font-bold">Projects</h1>
|
||||
<h1 class="text-4xl mt-4 font-bold">{% translate "Projects" %}</h1>
|
||||
</div>
|
||||
|
||||
{# align to bottom #}
|
||||
<div class="ml-auto"><!-- top, RHS (buttons) -->
|
||||
{% if can_create %}
|
||||
<div>
|
||||
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url 'project_new' %}">New Project</a>
|
||||
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url 'project_new' %}">{% translate "New Project" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div> {# top, RHS (buttons) #}
|
||||
@@ -38,10 +39,10 @@
|
||||
|
||||
<div class="flex bg-slate-50 dark:bg-slate-800 mt-4 items-end">
|
||||
<div class="flex">
|
||||
<a href="{% url "project_list_mine" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "mine" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">My Projects</div></a>
|
||||
<a href="{% url "project_list_mine" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "mine" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">{% translate "My Projects" %}</div></a>
|
||||
{% if not app_settings.SINGLE_USER %}
|
||||
<a href="{% url "project_list_teams" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "teams" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">Team Projects</div></a>
|
||||
<a href="{% url "project_list_other" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "other" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">Other Projects</div></a>
|
||||
<a href="{% url "project_list_teams" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "teams" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">{% translate "Team Projects" %}</div></a>
|
||||
<a href="{% url "project_list_other" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "other" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">{% translate "Other Projects" %}</div></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% comment %}
|
||||
@@ -70,19 +71,19 @@
|
||||
</div>
|
||||
<div>
|
||||
{{ project.team.name }}
|
||||
| {{ project.member_count }} members
|
||||
| {{ project.member_count }} {% translate "members" %}
|
||||
{# | {{ project.open_issue_count }} open issues #}
|
||||
{% if project.member %}
|
||||
| <a href="{% url 'project_member_settings' project_pk=project.id user_pk=request.user.id %}" class="font-bold text-cyan-500 dark:text-cyan-300">my settings</a>
|
||||
| <a href="{% url 'project_member_settings' project_pk=project.id user_pk=request.user.id %}" class="font-bold text-cyan-500 dark:text-cyan-300">{% translate "my settings" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="pr-2 text-center">
|
||||
{% if project.member %}
|
||||
{% if not project.member.accepted %}
|
||||
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">You're invited!</span>
|
||||
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm whitespace-nowrap">{% translate "You're invited!" %}</span>
|
||||
{% elif project.member.is_admin %} {# NOTE: we intentionally hide admin-ness for non-accepted users; #}
|
||||
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">Admin</span>
|
||||
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm whitespace-nowrap">{% translate "Admin" %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -150,17 +151,17 @@
|
||||
{% if project.member %}
|
||||
{% if not project.member.accepted %}
|
||||
<div>
|
||||
<a href="{% url 'project_members_accept' project_pk=project.id %}" class="font-bold text-cyan-500 dark:text-cyan-300">Invitation</a>
|
||||
<a href="{% url 'project_members_accept' project_pk=project.id %}" class="font-bold text-cyan-500 dark:text-cyan-300 whitespace-nowrap">{% translate "Invitation" %}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<button name="action" value="leave:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Leave</button>
|
||||
<button name="action" value="leave:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if ownership_filter == "teams" or project.is_joinable or request.user.is_superuser %}{# ownership_filter check: you can always join your own team's projects, so if you're looking at a list of them... #}
|
||||
<div>
|
||||
<button name="action" value="join:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 dark:hover:bg-slate-700 active:ring rounded-md">Join</button>
|
||||
<button name="action" value="join:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 dark:hover:bg-slate-700 active:ring rounded-md whitespace-nowrap">{% translate "Join" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -170,7 +171,7 @@
|
||||
{% empty %}
|
||||
<tr class="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 border-b-2">
|
||||
<td class="w-full p-4">
|
||||
No projects found.
|
||||
{% translate "No projects found." %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Member settings · {{ project.name }} · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "Member settings" %} · {{ project.name }} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -23,25 +24,25 @@
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl my-4 font-bold">Membership settings</h1>
|
||||
<h1 class="text-4xl my-4 font-bold">{% translate "Membership settings" %}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-4">
|
||||
{% if this_is_you %}
|
||||
Your membership settings for project "{{ project.name }}".
|
||||
{% blocktranslate with project_name=project.name %}Your membership settings for project "{{ project_name }}".{% endblocktranslate %}
|
||||
{% else %}
|
||||
Settings for project "{{ project.name }}" and user {{ user.username }}.
|
||||
{% blocktranslate with project_name=project.name username=user.username %}Settings for project "{{ project_name }}" and user {{ username }}.{% endblocktranslate %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% tailwind_formfield form.role %}
|
||||
{% tailwind_formfield form.send_email_alerts %}
|
||||
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
|
||||
{% if this_is_you %}
|
||||
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a> {# not quite perfect, because "you" can also click on yourself in the member list #}
|
||||
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a> {# not quite perfect, because "you" can also click on yourself in the member list #}
|
||||
{% else %}
|
||||
<a href="{% url "project_members" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
|
||||
<a href="{% url "project_members" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Members · {{ project.name }} · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -21,10 +22,10 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="flex">
|
||||
<h1 class="text-4xl mt-4 font-bold">Project Members</h1>
|
||||
<h1 class="text-4xl mt-4 font-bold">{% translate "Project Members" %}</h1>
|
||||
|
||||
<div class="ml-auto mt-6">
|
||||
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "project_members_invite" project_pk=project.pk %}">Invite Member</a>
|
||||
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "project_members_invite" project_pk=project.pk %}">{% translate "Invite Member" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,9 +46,9 @@
|
||||
<div>
|
||||
<a href="{% url "project_member_settings" project_pk=project.pk user_pk=member.user_id %}" class="text-xl text-cyan-500 font-bold">{{ member.user.email }}</a> {# "best name" perhaps later? #}
|
||||
{% if not member.accepted %}
|
||||
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">Invitation pending</span>
|
||||
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">{% translate "Invitation pending" %}</span>
|
||||
{% elif member.is_admin %} {# NOTE: we intentionally hide admin-ness for non-accepted users #}
|
||||
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">Admin</span>
|
||||
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">{% translate "Admin" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@@ -55,12 +56,12 @@
|
||||
<td class="p-4">
|
||||
<div class="flex justify-end">
|
||||
{% if not member.accepted %}
|
||||
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Reinvite</button>
|
||||
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Reinvite" %}</button>
|
||||
{% endif %}
|
||||
{% if request.user == member.user %}
|
||||
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Leave</button>
|
||||
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
|
||||
{% else %} {# NOTE: in our setup request_user_is_admin is implied because only admins may view the membership page #}
|
||||
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Remove</button>
|
||||
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Remove" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@@ -71,7 +72,7 @@
|
||||
<td class="w-full p-4">
|
||||
<div>
|
||||
{# Note: this is already somewhat exceptional, because the usually you'll at least see yourself here (unless you're a superuser and a project has become memberless) #}
|
||||
No members yet. <a href="{% url "project_members_invite" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Invite someone</a>.
|
||||
{% translate "No members yet." %} <a href="{% url "project_members_invite" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Invite someone</a>." %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -85,8 +86,8 @@
|
||||
|
||||
<div class="flex flex-direction-row">
|
||||
<div class="ml-auto py-8 pr-4">
|
||||
<a href="{% url "team_members" team_pk=project.team_id %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{{ project.team.name }} Members</a>
|
||||
<span class="font-bold text-slate-500 dark:text-slate-300">|</span> <a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Back to Projects</a>
|
||||
<a href="{% url "team_members" team_pk=project.team_id %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% blocktranslate with team_name=project.team.name %} {{ team_name }} Members{% endblocktranslate %}</a>
|
||||
<span class="font-bold text-slate-500 dark:text-slate-300">|</span> <a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Back to Projects" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Invitation · {{ project.name }} · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -14,15 +15,15 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl my-4 font-bold">Invitation to "{{ project.name }}"</h1>
|
||||
<h1 class="text-4xl my-4 font-bold">{% translate "Invitation to" %} "{{ project.name }}"</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-4">
|
||||
You have been invited to join the project "{{ project.name }}" in the role of "{{ membership.get_role_display }}". Please confirm by clicking the button below.
|
||||
{% blocktranslate with project_name=project.name role=membership.get_role_display %}You have been invited to join the project "{{ project_name }}" in the role of "{{ role }}". Please confirm by clicking the button below.{% endblocktranslate %}
|
||||
</div>
|
||||
|
||||
<button name="action" value="accept" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Accept</button>
|
||||
<button name="action" value="decline" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Decline</button>
|
||||
<button name="action" value="accept" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Accept" %}</button>
|
||||
<button name="action" value="decline" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Decline" %}</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Invite Members · {{ project.name }} · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -22,11 +23,11 @@
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl my-4 font-bold">Invite members ({{ project.name }})</h1>
|
||||
<h1 class="text-4xl my-4 font-bold">{% blocktranslate with project_name=project.name %}Invite members ({{ project_name }}){% endblocktranslate %}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-4">
|
||||
Invite a member to join the project "{{ project.name }}". They will receive an email with a link to join.
|
||||
{% blocktranslate with project_name=project.name %}Invite a member to join the project "{{ project_name }}". They will receive an email with a link to join.{% endblocktranslate %}
|
||||
</div>
|
||||
|
||||
{% tailwind_formfield_implicit form.email %}
|
||||
@@ -46,9 +47,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Invite Member</button>
|
||||
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Invite and add another</button>
|
||||
<a href="{% url "project_members" project_pk=project.pk %}" class="font-bold text-slate-500 dark:text-slate-300 ml-4">Cancel</a>
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Invite Member" %}</button>
|
||||
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Invite and add another" %}</button>
|
||||
<a href="{% url "project_members" project_pk=project.pk %}" class="font-bold text-slate-500 dark:text-slate-300 ml-4">{% translate "Cancel" %}</a>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}New project · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "New Project" %} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -14,7 +15,7 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl my-4 font-bold">New Project</h1>
|
||||
<h1 class="text-4xl my-4 font-bold">{% translate "New Project" %}</h1>
|
||||
</div>
|
||||
|
||||
{% tailwind_formfield form.team %}
|
||||
@@ -22,8 +23,8 @@
|
||||
{% tailwind_formfield form.visibility %}
|
||||
{% tailwind_formfield form.retention_max_event_count %}
|
||||
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
|
||||
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
|
||||
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.contrib import messages
|
||||
from django.contrib.auth import logout
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from users.models import EmailVerification
|
||||
from teams.models import TeamMembership, Team, TeamRole
|
||||
@@ -78,7 +79,7 @@ def project_list(request, ownership_filter=None):
|
||||
if not project.is_joinable(user=request.user) and not request.user.is_superuser:
|
||||
raise PermissionDenied("This project is not joinable")
|
||||
|
||||
messages.success(request, 'You have joined the project "%s"' % project.name)
|
||||
messages.success(request, _('You have joined the project "%s"') % project.name)
|
||||
ProjectMembership.objects.create(
|
||||
project_id=project_pk, user_id=request.user.id, role=ProjectRole.MEMBER, accepted=True)
|
||||
return redirect('project_member_settings', project_pk=project_pk, user_pk=request.user.id)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.template.defaultfilters import yesno
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bugsink.utils import assert_
|
||||
from .models import TeamRole, TeamMembership, Team
|
||||
@@ -9,9 +10,9 @@ User = get_user_model()
|
||||
|
||||
|
||||
class TeamMemberInviteForm(forms.Form):
|
||||
email = forms.EmailField(label='Email', required=True)
|
||||
email = forms.EmailField(label=_('Email'), required=True)
|
||||
role = forms.ChoiceField(
|
||||
label='Role', choices=TeamRole.choices, required=True, initial=TeamRole.MEMBER, widget=forms.RadioSelect)
|
||||
label=_('Role'), choices=TeamRole.choices, required=True, initial=TeamRole.MEMBER, widget=forms.RadioSelect)
|
||||
|
||||
def __init__(self, user_must_exist, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -37,6 +38,7 @@ class MyTeamMembershipForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
edit_role = kwargs.pop("edit_role")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
assert_(self.instance is not None, "This form is only implemented for editing")
|
||||
|
||||
@@ -44,7 +46,9 @@ class MyTeamMembershipForm(forms.ModelForm):
|
||||
del self.fields['role']
|
||||
|
||||
global_send_email_alerts = self.instance.user.send_email_alerts
|
||||
empty_label = "User-default (%s)" % yesno(global_send_email_alerts).capitalize()
|
||||
global_send_email_alerts_text = yesno(global_send_email_alerts).capitalize()
|
||||
|
||||
empty_label = _("User-default (%s)") % global_send_email_alerts_text
|
||||
self.fields['send_email_alerts'].empty_label = empty_label
|
||||
self.fields['send_email_alerts'].widget.choices[0] = ("unknown", empty_label)
|
||||
|
||||
|
||||
@@ -3,34 +3,36 @@ import uuid
|
||||
from django.db import models
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
|
||||
class TeamRole(models.IntegerChoices):
|
||||
MEMBER = 0
|
||||
ADMIN = 1
|
||||
MEMBER = 0, _("Member")
|
||||
ADMIN = 1, _("Admin")
|
||||
|
||||
|
||||
class TeamVisibility(models.IntegerChoices):
|
||||
# PUBLIC = 0 # anyone can see the team and its members not sure if I want this or always want to require click-in
|
||||
JOINABLE = 1 # anyone can join
|
||||
JOINABLE = 1, _("Joinable") # anyone can join
|
||||
|
||||
# the team's existance is visible in lists, but there is no "Join" button. the idea would be that you can "request
|
||||
# to join" (which is currently not implemented as a button, but you could do it 'out of bands' i.e. via email or
|
||||
# chat).
|
||||
DISCOVERABLE = 10
|
||||
DISCOVERABLE = 10, _("Discoverable")
|
||||
|
||||
# the team is not visible to non-members; you need to be invited
|
||||
HIDDEN = 99
|
||||
HIDDEN = 99, _("Hidden")
|
||||
|
||||
|
||||
class Team(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
name = models.CharField(max_length=255, blank=False, null=False, unique=True)
|
||||
name = models.CharField(pgettext_lazy("Team", "Name"), max_length=255, blank=False, null=False, unique=True)
|
||||
|
||||
visibility = models.IntegerField(
|
||||
_("Visibility"),
|
||||
choices=TeamVisibility.choices, default=TeamVisibility.DISCOVERABLE,
|
||||
help_text="Which users can see this team and its issues?")
|
||||
help_text=_("Which users can see this team and its issues?"))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -48,8 +50,8 @@ class TeamMembership(models.Model):
|
||||
team = models.ForeignKey(Team, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
|
||||
send_email_alerts = models.BooleanField(default=None, null=True, blank=True)
|
||||
role = models.IntegerField(choices=TeamRole.choices, default=TeamRole.MEMBER)
|
||||
send_email_alerts = models.BooleanField(_("Send email alerts"), default=None, null=True, blank=True)
|
||||
role = models.IntegerField(_("Role"), choices=TeamRole.choices, default=TeamRole.MEMBER)
|
||||
accepted = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Edit {{ team.name }} · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "Edit" %} {{ team.name }} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -14,18 +15,20 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl my-4 font-bold">Settings ({{ team.name }})</h1>
|
||||
<h1 class="text-4xl my-4 font-bold">{% blocktranslate with team_name=team.name %}Settings ({{ team_name }}){% endblocktranslate %}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-4">
|
||||
Team settings for "{{ team.name }}".
|
||||
{% blocktranslate with team_name=team.name %}Team settings for "{{ team_name }}".{% endblocktranslate %}
|
||||
</div>
|
||||
|
||||
{% tailwind_formfield form.name %}
|
||||
{% tailwind_formfield form.visibility %}
|
||||
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
|
||||
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
|
||||
<div class="flex items-center mt-4">
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
|
||||
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Teams · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "Teams" %} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -10,14 +11,14 @@
|
||||
<div class="m-4 flex flex-row items-end">
|
||||
|
||||
<div><!-- top, LHS (h1) -->
|
||||
<h1 class="text-4xl mt-4 font-bold">Teams</h1>
|
||||
<h1 class="text-4xl mt-4 font-bold">{% translate "Teams" %}</h1>
|
||||
</div>
|
||||
|
||||
{# align to bottom #}
|
||||
<div class="ml-auto"><!-- top, RHS (buttons) -->
|
||||
{% if can_create %}
|
||||
<div>
|
||||
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url 'team_new' %}">New Team</a>
|
||||
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url 'team_new' %}">{% translate "New Team" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div> {# top, RHS (buttons) #}
|
||||
@@ -28,8 +29,8 @@
|
||||
|
||||
<div class="flex bg-slate-50 dark:bg-slate-800 mt-4 items-end">
|
||||
<div class="flex">
|
||||
<a href="{% url "team_list_mine" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "mine" %}text-cyan-500 dark:text-cyan-400 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400 dark:hover:border-slate-500{% endif %}">My Teams</div></a>
|
||||
<a href="{% url "team_list_other" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "other" %}text-cyan-500 dark:text-cyan-400 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400 dark:hover:border-slate-500{% endif %}">Other Teams</div></a>
|
||||
<a href="{% url "team_list_mine" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "mine" %}text-cyan-500 dark:text-cyan-400 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400 dark:hover:border-slate-500{% endif %}">{% translate "My Teams" %}</div></a>
|
||||
<a href="{% url "team_list_other" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "other" %}text-cyan-500 dark:text-cyan-400 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400 dark:hover:border-slate-500{% endif %}">{% translate "Other Teams" %}</div></a>
|
||||
</div>
|
||||
{% comment %}
|
||||
<div class="ml-auto p-2">
|
||||
@@ -52,10 +53,10 @@
|
||||
{{ team.name }}
|
||||
</div>
|
||||
<div>
|
||||
{{ team.project_count }} projects
|
||||
| {{ team.member_count }} members
|
||||
{{ team.project_count }} {% translate "projects" %}
|
||||
| {{ team.member_count }} {% translate "members" %}
|
||||
{% if team.member %}
|
||||
| <a href="{% url 'team_member_settings' team_pk=team.id user_pk=request.user.id %}" class="font-bold text-cyan-500 dark:text-cyan-400">my settings</a>
|
||||
| <a href="{% url 'team_member_settings' team_pk=team.id user_pk=request.user.id %}" class="font-bold text-cyan-500 dark:text-cyan-400">{% translate "my settings" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@@ -65,7 +66,7 @@
|
||||
{% if not team.member.accepted %}
|
||||
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">You're invited!</span>
|
||||
{% elif team.member.is_admin %} {# NOTE: we intentionally hide admin-ness for non-accepted users #}
|
||||
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">Admin</span>
|
||||
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm whitespace-nowrap">{% translate "Admin" %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -100,17 +101,17 @@
|
||||
{% if team.member %}
|
||||
{% if not team.member.accepted %}
|
||||
<div>
|
||||
<a href="{% url 'team_members_accept' team_pk=team.id %}" class="font-bold text-cyan-500 dark:text-cyan-400">Invitation</a>
|
||||
<a href="{% url 'team_members_accept' team_pk=team.id %}" class="font-bold text-cyan-500 dark:text-cyan-400 whitespace-nowrap">{% translate "Invitation" %}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<button name="action" value="leave:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Leave</button>
|
||||
<button name="action" value="leave:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if team.is_joinable or request.user.is_superuser %}
|
||||
<div>
|
||||
<button name="action" value="join:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Join</button>
|
||||
<button name="action" value="join:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Join" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -120,7 +121,7 @@
|
||||
{% empty %}
|
||||
<tr class="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 border-b-2">
|
||||
<td class="w-full p-4">
|
||||
No teams found.
|
||||
{% translate "No teams found." %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Member settings · {{ team.name }} · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -23,25 +24,25 @@
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl my-4 font-bold">Membership settings</h1>
|
||||
<h1 class="text-4xl my-4 font-bold">{% trans "Membership settings" %}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-4">
|
||||
{% if this_is_you %}
|
||||
Your membership settings for team "{{ team.name }}".
|
||||
{% blocktrans with team_name=team.name %}Your membership settings for team "{{ team_name }}".{% endblocktrans %}
|
||||
{% else %}
|
||||
Settings for team "{{ team.name }}" and user {{ user.username }}.
|
||||
{% blocktrans with team_name=team.name username=user.username %}Settings for team "{{ team_name }}" and user {{ username }}.{% endblocktrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% tailwind_formfield form.role %}
|
||||
{% tailwind_formfield form.send_email_alerts %}
|
||||
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
|
||||
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
|
||||
{% if this_is_you %}
|
||||
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a> {# not quite perfect, because "you" can also click on yourself in the member list #}
|
||||
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a> {# not quite perfect, because "you" can also click on yourself in the member list #}
|
||||
{% else %}
|
||||
<a href="{% url "team_members" team_pk=team.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
|
||||
<a href="{% url "team_members" team_pk=team.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Members · {{ team.name }} · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "Members" %} · {{ team.name }} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -21,10 +22,10 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="flex">
|
||||
<h1 class="text-4xl mt-4 font-bold">Team Members</h1>
|
||||
<h1 class="text-4xl mt-4 font-bold">{% translate "Team Members" %}</h1>
|
||||
|
||||
<div class="ml-auto mt-6">
|
||||
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "team_members_invite" team_pk=team.pk %}">Invite Member</a>
|
||||
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "team_members_invite" team_pk=team.pk %}">{% translate "Invite Member" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,9 +46,9 @@
|
||||
<div>
|
||||
<a href="{% url "team_member_settings" team_pk=team.pk user_pk=member.user_id %}" class="text-xl text-cyan-500 dark:text-cyan-300 font-bold">{{ member.user.email }}</a> {# "best name" perhaps later? #}
|
||||
{% if not member.accepted %}
|
||||
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">Invitation pending</span>
|
||||
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">{% translate "Invitation pending" %}</span>
|
||||
{% elif member.is_admin %} {# NOTE: we intentionally hide admin-ness for non-accepted users #}
|
||||
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">Admin</span>
|
||||
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">{% translate "Admin" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@@ -55,12 +56,12 @@
|
||||
<td class="p-4">
|
||||
<div class="flex justify-end">
|
||||
{% if not member.accepted %}
|
||||
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Reinvite</button>
|
||||
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Reinvite" %}</button>
|
||||
{% endif %}
|
||||
{% if request.user == member.user %}
|
||||
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Leave</button>
|
||||
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
|
||||
{% else %} {# NOTE: in our setup request_user_is_admin is implied because only admins may view the membership page #}
|
||||
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Remove</button>
|
||||
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Remove" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@@ -71,7 +72,7 @@
|
||||
<td class="w-full p-4">
|
||||
<div>
|
||||
{# Note: this is already somewhat exceptional, because the usually you'll at least see yourself here (unless you're a superuser and a team has become memberless) #}
|
||||
No members yet. <a href="{% url "team_members_invite" team_pk=team.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Invite someone</a>.
|
||||
{% translate "No members yet." %} <a href="{% url "team_members_invite" team_pk=team.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Invite someone</a>." %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -85,7 +86,7 @@
|
||||
|
||||
<div class="flex flex-direction-row">
|
||||
<div class="ml-auto py-8 pr-4">
|
||||
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Back to Teams</a>
|
||||
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Back to Teams" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Invite Members · {{ team.name }} · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -24,11 +25,11 @@
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl mt-4 font-bold">Invite members ({{ team.name }})</h1>
|
||||
<h1 class="text-4xl mt-4 font-bold">{% blocktranslate with team_name=team.name %}Invite members ({{ team_name }}){% endblocktranslate %}</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-4">
|
||||
Invite a member to join the team "{{ team.name }}". They will receive an email with a link to join.
|
||||
{% blocktranslate with team_name=team.name %}Invite a member to join the team "{{ team_name }}". They will receive an email with a link to join.{% endblocktranslate %}
|
||||
</div>
|
||||
|
||||
{% tailwind_formfield_implicit form.email %}
|
||||
@@ -48,9 +49,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Invite Member</button>
|
||||
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Invite and add another</button>
|
||||
<a href="{% url "team_members" team_pk=team.pk %}" class="font-bold text-slate-500 dark:text-slate-300 ml-4">Cancel</a>
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Invite Member" %}</button>
|
||||
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Invite and add another" %}</button>
|
||||
<a href="{% url "team_members" team_pk=team.pk %}" class="font-bold text-slate-500 dark:text-slate-300 ml-4">{% translate "Cancel" %}</a>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}New team · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -14,14 +15,14 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl my-4 font-bold">New Team</h1>
|
||||
<h1 class="text-4xl my-4 font-bold">{% translate "New Team" %}</h1>
|
||||
</div>
|
||||
|
||||
{% tailwind_formfield form.name %}
|
||||
{% tailwind_formfield form.visibility %}
|
||||
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
|
||||
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
|
||||
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import logout
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from users.models import EmailVerification
|
||||
from bugsink.app_settings import get_settings, CB_ANYBODY, CB_ADMINS, CB_MEMBERS
|
||||
@@ -54,7 +55,7 @@ def team_list(request, ownership_filter=None):
|
||||
if not team.is_joinable() and not request.user.is_superuser:
|
||||
raise PermissionDenied("This team is not joinable")
|
||||
|
||||
messages.success(request, 'You have joined the team "%s"' % team.name)
|
||||
messages.success(request, _('You have joined the team "%s"') % team.name)
|
||||
TeamMembership.objects.create(team_id=team_pk, user_id=request.user.id, role=TeamRole.MEMBER, accepted=True)
|
||||
return redirect('team_member_settings', team_pk=team_pk, user_pk=request.user.id)
|
||||
|
||||
@@ -126,7 +127,8 @@ def team_edit(request, team_pk):
|
||||
|
||||
if action == 'delete':
|
||||
# Double-check that the user is an admin or superuser
|
||||
if not (TeamMembership.objects.filter(team=team, user=request.user, role=TeamRole.ADMIN, accepted=True).exists() or
|
||||
if not (TeamMembership.objects.filter(
|
||||
team=team, user=request.user, role=TeamRole.ADMIN, accepted=True).exists() or
|
||||
request.user.is_superuser):
|
||||
raise PermissionDenied("Only team admins can delete teams")
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "barest_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Log in · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -14,13 +15,13 @@
|
||||
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="mb-8 text-red-500 dark:text-red-400">Your username and password didn't match. Please try again.</div>
|
||||
<div class="mb-8 text-red-500 dark:text-red-400">{% translate "Your username and password didn't match. Please try again." %}</div>
|
||||
|
||||
{% elif next %}
|
||||
{% if user.is_authenticated %}
|
||||
<div class="mb-8">Your account doesn't have access to this page. To proceed, please login with an account that has access.</div>
|
||||
<div class="mb-8">{% translate "Your account doesn't have access to this page. To proceed, please login with an account that has access." %}</div>
|
||||
{% else %}
|
||||
<div class="mb-8">Please login to see this page.</div>
|
||||
<div class="mb-8">{% translate "Please login to see this page." %}</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -30,22 +31,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="absolute ml-3" width="24">
|
||||
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<input name="username" type="text" class="bg-slate-200 dark:bg-slate-800 pl-12 py-2 md:py-4 focus:outline-none w-full" {% if form.username.value %}value="{{ form.username.value }}"{% endif %} placeholder="Username" />
|
||||
<input name="username" type="text" class="bg-slate-200 dark:bg-slate-800 pl-12 py-2 md:py-4 focus:outline-none w-full" {% if form.username.value %}value="{{ form.username.value }}"{% endif %} placeholder="{% translate 'Username' %}" />
|
||||
</div>
|
||||
<div class="flex items-center text-lg mb-6 md:mb-8">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="absolute ml-3" width="24">
|
||||
<path fill-rule="evenodd" d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<input name="password" type="password" class="bg-slate-200 dark:bg-slate-800 pl-12 py-2 md:py-4 focus:outline-none w-full" {% if form.password.value %}value="{{ form.password.value }}"{% endif %} placeholder="Password" />
|
||||
<input name="password" type="password" class="bg-slate-200 dark:bg-slate-800 pl-12 py-2 md:py-4 focus:outline-none w-full" {% if form.password.value %}value="{{ form.password.value }}"{% endif %} placeholder="{% translate 'Password' %}" />
|
||||
</div>
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Login</button>
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">{% translate "Login" %}</button>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{% url 'request_reset_password' %}" class="text-slate-800 dark:text-slate-100">Forgot password?</a>
|
||||
{% if registration_enabled %}<a href="{% url 'signup' %}" class="float-right text-slate-800 dark:text-slate-100">Create an account</a>{% endif %}
|
||||
<a href="{% url 'request_reset_password' %}" class="text-slate-800 dark:text-slate-100">{% translate "Forgot password?" %}</a>
|
||||
{% if registration_enabled %}<a href="{% url 'signup' %}" class="float-right text-slate-800 dark:text-slate-100">{% translate "Create an account" %}</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load stricter_templates %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Settings · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "Settings" %} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="m-4">
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl my-4 font-bold">Site Settings</h1>
|
||||
<h1 class="text-4xl my-4 font-bold">{% translate "Site Settings" %}</h1>
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl font-bold mt-4">Bugsink</h1>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% load static tailwind_tags version add_to_qs %}<!DOCTYPE html>
|
||||
{% load static tailwind_tags version add_to_qs %}{% load i18n %}<!DOCTYPE html>
|
||||
<html lang="en" data-theme="{% if user.is_authenticated %}{{ user.theme_preference }}{% else %}dark{% endif %}">
|
||||
<!-- version: {% version %} -->
|
||||
<head>
|
||||
@@ -31,30 +31,30 @@
|
||||
<a href="/"><div class="px-2 py-2 my-2 font-bold hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{{ site_title }}</div></a>
|
||||
|
||||
{% if not app_settings.SINGLE_TEAM %}
|
||||
<a href="{% url "team_list" %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Teams</div></a>
|
||||
<a href="{% url "team_list" %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Teams" %}</div></a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url "project_list" %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Projects</div></a>
|
||||
<a href="{% url "project_list" %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Projects" %}</div></a>
|
||||
|
||||
{% if project %}
|
||||
<a href="{% url "issue_list_open" project_pk=project.pk %}{% current_qs %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Issues ({{ project.name }})</div></a>
|
||||
<a href="{% url "issue_list_open" project_pk=project.pk %}{% current_qs %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Issues" %} ({{ project.name }})</div></a>
|
||||
{% endif %}
|
||||
|
||||
<div class="ml-auto flex">
|
||||
{% if app_settings.USE_ADMIN and user.is_staff %}
|
||||
<a href="/admin/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Admin</div></a>
|
||||
<a href="/admin/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Admin" %}</div></a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_superuser %}
|
||||
<a href="/users/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Users</div></a>
|
||||
<a href="/bsmain/auth_tokens/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Tokens</div></a>
|
||||
<a href="/users/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Users" %}</div></a>
|
||||
<a href="/bsmain/auth_tokens/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Tokens" %}</div></a>
|
||||
{% endif %}
|
||||
|
||||
{% if logged_in_user.is_anonymous %}
|
||||
<a href="/accounts/login/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Login</div></a> {# I don't think this is actually ever shown in practice, because you must always be logged in #}
|
||||
<a href="/accounts/login/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Login" %}</div></a> {# I don't think this is actually ever shown in practice, because you must always be logged in #}
|
||||
{% else %}
|
||||
<a href="/accounts/preferences/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Preferences</div></a>
|
||||
<div class="px-4 py-2 my-2 mr-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl"><form id="logout-form" method="post" action="{% url 'logout' %}">{% csrf_token %}<button type="submit">Log out</button></form></div>
|
||||
<a href="/accounts/preferences/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Preferences" %}</div></a>
|
||||
<div class="px-4 py-2 my-2 mr-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl"><form id="logout-form" method="post" action="{% url 'logout' %}">{% csrf_token %}<button type="submit">{% translate "Log out" %}</button></form></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,19 +9,16 @@ from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth import password_validation
|
||||
from django.forms import ModelForm
|
||||
from django.utils.html import escape, mark_safe
|
||||
from django.utils.translation import gettext_lazy as _, get_language_info
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
TRUE_FALSE_CHOICES = (
|
||||
(True, 'Yes'),
|
||||
(False, 'No')
|
||||
(True, _("Yes")),
|
||||
(False, _("No"))
|
||||
)
|
||||
|
||||
|
||||
def _(x):
|
||||
# dummy gettext
|
||||
return x
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@@ -87,7 +84,7 @@ class UserEditForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['username'].validators = [EmailValidator()]
|
||||
self.fields['username'].label = "Email"
|
||||
self.fields['username'].label = _("Email")
|
||||
|
||||
self.fields['username'].help_text = None # "Email" is descriptive enough
|
||||
|
||||
@@ -136,6 +133,18 @@ class SetPasswordForm(BaseSetPasswordForm):
|
||||
self.fields['new_password2'].help_text = None # "Confirm password" is descriptive enough
|
||||
|
||||
|
||||
def language_choices():
|
||||
items = [("auto", _("Auto (browser preference)"))]
|
||||
|
||||
for code, _label in settings.LANGUAGES:
|
||||
info = get_language_info(code)
|
||||
label = info["name_local"] \
|
||||
if info["name_local"] == info["name_translated"] \
|
||||
else f"{info['name_local']} ({info['name_translated']})"
|
||||
items.append((code, label))
|
||||
return items
|
||||
|
||||
|
||||
class PreferencesForm(ModelForm):
|
||||
# I haven't gotten a decent display for checkboxes in forms yet; the quickest hack around this is a ChoiceField
|
||||
send_email_alerts = forms.ChoiceField(
|
||||
@@ -146,7 +155,13 @@ class PreferencesForm(ModelForm):
|
||||
required=True,
|
||||
widget=forms.Select(),
|
||||
)
|
||||
language = forms.ChoiceField(
|
||||
label=_("Language"),
|
||||
choices=language_choices,
|
||||
required=True,
|
||||
widget=forms.Select(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("send_email_alerts", "theme_preference",)
|
||||
fields = ("send_email_alerts", "theme_preference", "language",)
|
||||
|
||||
16
users/migrations/0003_user_language.py
Normal file
16
users/migrations/0003_user_language.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0002_user_theme_preference"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="language",
|
||||
field=models.CharField(default="auto", max_length=10),
|
||||
),
|
||||
]
|
||||
@@ -3,6 +3,7 @@ import secrets
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
@@ -18,9 +19,9 @@ class User(AbstractUser):
|
||||
send_email_alerts = models.BooleanField(default=True, blank=True)
|
||||
|
||||
THEME_CHOICES = [
|
||||
("system", "System Default"),
|
||||
("light", "Light"),
|
||||
("dark", "Dark"),
|
||||
("system", _("System Default")),
|
||||
("light", _("Light")),
|
||||
("dark", _("Dark")),
|
||||
]
|
||||
theme_preference = models.CharField(
|
||||
max_length=10,
|
||||
@@ -28,6 +29,14 @@ class User(AbstractUser):
|
||||
default="system",
|
||||
blank=False,
|
||||
)
|
||||
language = models.CharField(
|
||||
max_length=10,
|
||||
# choices intentionally not set, we don't want changes to trigger migrations; the actual choices are set in
|
||||
# forms.py; in Django 5.0 and up we can instead used a callable here
|
||||
# choices=...
|
||||
default="auto",
|
||||
blank=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'auth_user'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "barest_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Confirm email · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -14,12 +15,12 @@
|
||||
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
|
||||
|
||||
<div class="mb-8">
|
||||
Confirm your email address by clicking the button below.
|
||||
{% translate "Confirm your email address by clicking the button below." %}
|
||||
</div>
|
||||
|
||||
<form method="post" action=".">
|
||||
{% csrf_token %}
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Confirm</button>
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">{% translate "Confirm" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "barest_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Verification email sent · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -13,7 +14,7 @@
|
||||
|
||||
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
|
||||
|
||||
A verification email has been sent to your email address. Please verify your email address to complete the registration process.
|
||||
{% translate "A verification email has been sent to your email address. Please verify your email address to complete the registration process." %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "barest_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Logged out · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -12,7 +13,7 @@
|
||||
</div>
|
||||
|
||||
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
|
||||
You have been logged out. <a href="{% url 'login' %}" class="text-cyan-500 dark:text-cyan-300">Log in again</a>.
|
||||
{% translate "You have been logged out." %} <a href="{% url 'login' %}" class="text-cyan-500 dark:text-cyan-300">{% translate "Log in again</a>." %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}User Preferences · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -23,13 +24,14 @@
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<h1 class="text-4xl my-4 font-bold">User Preferences</h1>
|
||||
<h1 class="text-4xl my-4 font-bold">{% translate "User Preferences" %}</h1>
|
||||
</div>
|
||||
|
||||
{% tailwind_formfield form.send_email_alerts %}
|
||||
{% tailwind_formfield form.theme_preference %}
|
||||
{% tailwind_formfield form.language %}
|
||||
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "barest_base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Resend confirmation · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -19,11 +20,11 @@
|
||||
|
||||
{% tailwind_formfield_implicit form.email %}
|
||||
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Reset password</button>
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">{% translate "Reset password" %}</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{% url 'login' %}" class="text-slate-800 dark:text-slate-100">Log in instead</a>
|
||||
<a href="{% url 'login' %}" class="text-slate-800 dark:text-slate-100">{% translate "Log in instead" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "barest_base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Resend confirmation · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -19,7 +20,7 @@
|
||||
|
||||
{% tailwind_formfield_implicit form.email %}
|
||||
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Resend verification email</button>
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">{% translate "Resend verification email" %}</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "barest_base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Reset password · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -23,12 +24,12 @@
|
||||
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Reset password</button>
|
||||
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">{% translate "Reset password" %}</button>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{% url 'login' %}" class="text-slate-800 dark:text-slate-100">Log in instead</a>
|
||||
<a href="{% url 'login' %}" class="text-slate-800 dark:text-slate-100">{% translate "Log in instead" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "barest_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Password reset sent · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -12,7 +13,7 @@
|
||||
</div>
|
||||
|
||||
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
|
||||
A password reset link has been sent to your email address. Please check your inbox and follow the instructions to reset your password.
|
||||
{% translate "A password reset link has been sent to your email address. Please check your inbox and follow the instructions to reset your password." %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load tailwind_forms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Edit {{ form.instance.username }} · {{ site_title }}{% endblock %}
|
||||
{% block title %}{% translate "Edit" %} {{ form.instance.username }} · {{ site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -18,13 +19,13 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-4">
|
||||
Settings for "{{ form.instance.username }}".
|
||||
{% blocktranslate with username=form.instance.username %}Settings for {{ username }}.{% endblocktranslate %}
|
||||
</div>
|
||||
|
||||
{% tailwind_formfield form.username %}
|
||||
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
|
||||
<a href="{% url "user_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
|
||||
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
|
||||
<a href="{% url "user_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Users · {{ site_title }}{% endblock %}
|
||||
|
||||
@@ -12,7 +13,7 @@
|
||||
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">Delete User</h3>
|
||||
<div class="mt-4 mb-6">
|
||||
<p class="text-slate-700 dark:text-slate-300">
|
||||
Are you sure you want to delete this user? This action cannot be undone.
|
||||
{% translate "Are you sure you want to delete this user? This action cannot be undone." %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-center space-x-4 mb-4">
|
||||
@@ -41,7 +42,7 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="flex">
|
||||
<h1 class="text-4xl mt-4 font-bold">Users</h1>
|
||||
<h1 class="text-4xl mt-4 font-bold">{% translate "Users" %}</h1>
|
||||
{% comment %}
|
||||
Our current invite-system is tied to either a team or a project; no "global" invites (yet).
|
||||
<div class="ml-auto mt-6">
|
||||
@@ -58,7 +59,7 @@
|
||||
<tbody>
|
||||
<thead>
|
||||
<tr class="bg-slate-200 dark:bg-slate-800">
|
||||
<th class="w-full p-4 text-left text-xl" colspan="2">Users</th>
|
||||
<th class="w-full p-4 text-left text-xl" colspan="2">{% translate "Users" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for user in users %}
|
||||
@@ -67,7 +68,7 @@
|
||||
<div>
|
||||
<a href="{% url "user_edit" user_pk=user.pk %}" class="text-xl text-cyan-500 dark:text-cyan-300 font-bold">{{ user.username }}</a>
|
||||
{% if member.is_superuser %}
|
||||
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">Superuser</span>
|
||||
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">{% translate "Superuser" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -7,9 +7,12 @@ from django.http import Http404
|
||||
from django.utils import timezone
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils import translation
|
||||
|
||||
from bugsink.app_settings import get_settings, CB_ANYBODY
|
||||
from bugsink.decorators import atomic_for_request_method
|
||||
from bugsink.middleware import get_chosen_language
|
||||
|
||||
from .forms import (
|
||||
UserCreationForm, ResendConfirmationForm, RequestPasswordResetForm, SetPasswordForm, PreferencesForm, UserEditForm)
|
||||
@@ -225,8 +228,13 @@ def preferences(request):
|
||||
form = PreferencesForm(request.POST, instance=user)
|
||||
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, "Updated preferences")
|
||||
user = form.save()
|
||||
|
||||
# activate the selected language immediately for the Success message; we've already passed the middleware
|
||||
# stage (which looked at the pre-change language), so we need to do this ourselves with the fresh value.
|
||||
translation.activate(get_chosen_language(user, request))
|
||||
|
||||
messages.success(request, _("Updated preferences"))
|
||||
return redirect('preferences')
|
||||
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user