diff --git a/accounts/tests/test_api_tokens.py b/accounts/tests/test_api_tokens.py index 177a0635..51a6a2b0 100644 --- a/accounts/tests/test_api_tokens.py +++ b/accounts/tests/test_api_tokens.py @@ -138,6 +138,14 @@ class ApiTokenTests(TestCase): self.assertNotRegex(response.content.decode(), r"wvc_[0-9a-f]{64}") self.assertIn("at most 10", response.content.decode()) + def test_a_form_sent_twice_makes_one_token(self): + self.make("ci") + response = self.client.post(reverse("accounts:api_token_create"), {"name": "ci", "days": "90"}) + self.assertContains(response, "You have a token named ci: choose another name.") + self.assertEqual(ApiToken.objects.filter(user=self.user).count(), 1) + self.make("ci-2") # another name is fine + self.assertEqual(ApiToken.objects.filter(user=self.user).count(), 2) + def test_expired_tokens_do_not_count_toward_the_limit(self): for number in range(10): self.make(name=f"t{number}") diff --git a/accounts/views.py b/accounts/views.py index 86ae8ebc..817ce360 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -69,13 +69,21 @@ def api_token_create(request): form = ApiTokenForm(request.POST) if not form.is_valid(): return render(request, "common/form.html", {"form": form, "title": _("Create an API token")}) - ApiToken.objects.filter(user=request.user, expiry__lt=timezone.now()).delete() - if ApiToken.objects.filter(user=request.user).count() >= ApiToken.LIMIT: - form.add_error(None, _("A user has at most %(limit)s API tokens: delete one first.") % {"limit": ApiToken.LIMIT}) - return render(request, "common/form.html", {"form": form, "title": _("Create an API token")}) name, days = form.cleaned_data["name"], form.cleaned_data["days"] - with audit(request, None, f"API token {name}", "add"): - _token, key = ApiToken.objects.create(request.user, expiry=timedelta(days=days) if days else None, name=name) + with transaction.atomic(): + # the user's row: two forms sent at once (a double click) take their turns + get_user_model().objects.select_for_update().get(pk=request.user.pk) + ApiToken.objects.filter(user=request.user, expiry__lt=timezone.now()).delete() + if ApiToken.objects.filter(user=request.user).count() >= ApiToken.LIMIT: + form.add_error(None, _("A user has at most %(limit)s API tokens: delete one first.") % {"limit": ApiToken.LIMIT}) + elif ApiToken.objects.filter(user=request.user, name=name).exists(): + form.add_error("name", _("You have a token named %(name)s: choose another name.") % {"name": name}) + else: + with audit(request, None, f"API token {name}", "add"): + _token, key = ApiToken.objects.create(request.user, expiry=timedelta(days=days) if days else None, + name=name) + if form.errors: + return render(request, "common/form.html", {"form": form, "title": _("Create an API token")}) # shown by a GET, so reloading the page does not create another token request.session["api_token_created"] = {"name": name, "key": key} return redirect("accounts:api_token_created") diff --git a/locale/tr/LC_MESSAGES/django.mo b/locale/tr/LC_MESSAGES/django.mo index 0d026955..4c3e1c0c 100644 Binary files a/locale/tr/LC_MESSAGES/django.mo and b/locale/tr/LC_MESSAGES/django.mo differ diff --git a/locale/tr/LC_MESSAGES/django.po b/locale/tr/LC_MESSAGES/django.po index 2dfa1215..8954b854 100644 --- a/locale/tr/LC_MESSAGES/django.po +++ b/locale/tr/LC_MESSAGES/django.po @@ -523,7 +523,7 @@ msgid "Add" msgstr "Ekle" #: accounts/templates/profile.html:64 accounts/templates/profile.html:70 -#: accounts/views.py:71 accounts/views.py:75 +#: accounts/views.py:71 accounts/views.py:86 msgid "Create an API token" msgstr "API anahtarı oluştur" @@ -568,85 +568,90 @@ msgstr "" msgid "Profile updated" msgstr "Profil güncellendi" -#: accounts/views.py:74 +#: accounts/views.py:78 #, python-format msgid "A user has at most %(limit)s API tokens: delete one first." msgstr "" "Bir kullanıcının en fazla %(limit)s API anahtarı olabilir: önce birini silin." -#: accounts/views.py:109 +#: accounts/views.py:80 +#, python-format +msgid "You have a token named %(name)s: choose another name." +msgstr "%(name)s adlı bir token'ınız var: başka bir ad seçin." + +#: accounts/views.py:117 msgid "API token deleted" msgstr "API anahtarı silindi" -#: accounts/views.py:117 +#: accounts/views.py:125 msgid "Delete API token" msgstr "API anahtarını sil" -#: accounts/views.py:118 +#: accounts/views.py:126 msgid "Scripts and tools that use it can no longer reach the API." msgstr "Onu kullanan betikler ve araçlar artık API'ye erişemez." -#: accounts/views.py:128 +#: accounts/views.py:136 msgid "SSH key added" msgstr "SSH anahtarı eklendi" -#: accounts/views.py:136 +#: accounts/views.py:144 msgid "Add SSH key" msgstr "SSH Anahtarı Ekle" -#: accounts/views.py:157 +#: accounts/views.py:165 msgid "SSH key deleted" msgstr "SSH anahtarı silindi" -#: accounts/views.py:165 +#: accounts/views.py:173 msgid "Delete SSH key" msgstr "SSH anahtarı sil" -#: accounts/views.py:166 +#: accounts/views.py:174 msgid "" "VMs that already have it keep it; VMs created from now on do not get it." msgstr "" "Ona zaten sahip olan sanal makinelerde kalır; bundan sonra oluşturulan sanal " "makinelere eklenmez." -#: accounts/views.py:215 +#: accounts/views.py:223 msgid "Password Changed" msgstr "Parola Değişti" -#: accounts/views.py:244 +#: accounts/views.py:252 msgid "Create User Instance" msgstr "Yeni Kullanıcı Sanal Makinesi Oluştur" -#: accounts/views.py:278 +#: accounts/views.py:286 msgid "Update User Instance" msgstr "Kullanıcı Sanal Makinesini Güncelle" -#: accounts/views.py:296 +#: accounts/views.py:304 msgid "The user loses access to this VM; the VM itself is not changed." msgstr "" "Kullanıcı bu sanal makineye erişimini kaybeder; sanal makinenin kendisi " "değişmez." -#: accounts/views.py:329 +#: accounts/views.py:337 #, python-format msgid "OTP Sent to %(email)s" msgstr "OTP %(email)s ye gönderildi" -#: accounts/views.py:338 +#: accounts/views.py:346 msgid "Email OTP" msgstr "OTP E-Postası" -#: accounts/views.py:372 +#: accounts/views.py:380 #, python-format msgid "The OTP QR code could not be mailed to %(user)s: %(error)s" msgstr "OTP QR kodu %(user)s kullanıcısına e-postayla gönderilemedi: %(error)s" -#: accounts/views.py:376 +#: accounts/views.py:384 #, python-format msgid "OTP QR code was emailed to user %(user)s" msgstr "OTP QR kodu %(user)s kullanıcısına gönderildi" -#: accounts/views.py:380 +#: accounts/views.py:388 msgid "User email not set, failed to send QR code" msgstr "Kullanıcı e-postası girilmemiş, QR kod gönderimi başarısız" diff --git a/locale/zh/LC_MESSAGES/django.mo b/locale/zh/LC_MESSAGES/django.mo index 386a1d2b..ac533788 100644 Binary files a/locale/zh/LC_MESSAGES/django.mo and b/locale/zh/LC_MESSAGES/django.mo differ diff --git a/locale/zh/LC_MESSAGES/django.po b/locale/zh/LC_MESSAGES/django.po index af9bbe07..c1de38de 100644 --- a/locale/zh/LC_MESSAGES/django.po +++ b/locale/zh/LC_MESSAGES/django.po @@ -517,7 +517,7 @@ msgid "Add" msgstr "添加" #: accounts/templates/profile.html:64 accounts/templates/profile.html:70 -#: accounts/views.py:71 accounts/views.py:75 +#: accounts/views.py:71 accounts/views.py:86 msgid "Create an API token" msgstr "创建 API 令牌" @@ -560,80 +560,85 @@ msgstr "" msgid "Profile updated" msgstr "资料已更新" -#: accounts/views.py:74 +#: accounts/views.py:78 #, python-format msgid "A user has at most %(limit)s API tokens: delete one first." msgstr "每个用户最多有 %(limit)s 个 API 令牌:请先删除一个。" -#: accounts/views.py:109 +#: accounts/views.py:80 +#, python-format +msgid "You have a token named %(name)s: choose another name." +msgstr "您已有名为 %(name)s 的令牌:请换一个名称。" + +#: accounts/views.py:117 msgid "API token deleted" msgstr "API 令牌已删除" -#: accounts/views.py:117 +#: accounts/views.py:125 msgid "Delete API token" msgstr "删除 API 令牌" -#: accounts/views.py:118 +#: accounts/views.py:126 msgid "Scripts and tools that use it can no longer reach the API." msgstr "使用它的脚本和工具将无法再访问 API。" -#: accounts/views.py:128 +#: accounts/views.py:136 msgid "SSH key added" msgstr "SSH 密钥已添加" -#: accounts/views.py:136 +#: accounts/views.py:144 msgid "Add SSH key" msgstr "添加 SSH 密钥" -#: accounts/views.py:157 +#: accounts/views.py:165 msgid "SSH key deleted" msgstr "SSH 密钥已删除" -#: accounts/views.py:165 +#: accounts/views.py:173 msgid "Delete SSH key" msgstr "删除 SSH 密钥" -#: accounts/views.py:166 +#: accounts/views.py:174 msgid "" "VMs that already have it keep it; VMs created from now on do not get it." msgstr "已有此密钥的虚拟机会保留它;此后创建的虚拟机不会再获得它。" -#: accounts/views.py:215 +#: accounts/views.py:223 msgid "Password Changed" msgstr "密码已修改" -#: accounts/views.py:244 +#: accounts/views.py:252 msgid "Create User Instance" msgstr "创建用户虚拟机" -#: accounts/views.py:278 +#: accounts/views.py:286 msgid "Update User Instance" msgstr "更新用户虚拟机" -#: accounts/views.py:296 +#: accounts/views.py:304 msgid "The user loses access to this VM; the VM itself is not changed." msgstr "该用户将失去对此虚拟机的访问权限;虚拟机本身不受影响。" -#: accounts/views.py:329 +#: accounts/views.py:337 #, python-format msgid "OTP Sent to %(email)s" msgstr "OTP 已发送至 %(email)s" -#: accounts/views.py:338 +#: accounts/views.py:346 msgid "Email OTP" msgstr "邮件发送 OTP" -#: accounts/views.py:372 +#: accounts/views.py:380 #, python-format msgid "The OTP QR code could not be mailed to %(user)s: %(error)s" msgstr "无法将 OTP 二维码通过邮件发送给 %(user)s:%(error)s" -#: accounts/views.py:376 +#: accounts/views.py:384 #, python-format msgid "OTP QR code was emailed to user %(user)s" msgstr "OTP 二维码已通过邮件发送给用户 %(user)s" -#: accounts/views.py:380 +#: accounts/views.py:388 msgid "User email not set, failed to send QR code" msgstr "未设置用户邮箱,无法发送二维码"