wip, claude + docker-compose modifications
This commit is contained in:
11
app.py
11
app.py
@@ -12,9 +12,15 @@ SUPPORTED_LANGS = ["fr", "en", "de"]
|
||||
|
||||
|
||||
def _select_locale():
|
||||
# 1. Explicit session override (set via flag switcher)
|
||||
lang = flask_session.get("lang")
|
||||
if lang in SUPPORTED_LANGS:
|
||||
return lang
|
||||
# 2. Authenticated user's stored preference
|
||||
if current_user.is_authenticated and current_user.language in SUPPORTED_LANGS:
|
||||
flask_session["lang"] = current_user.language
|
||||
return current_user.language
|
||||
# 3. Browser Accept-Language header
|
||||
return request.accept_languages.best_match(SUPPORTED_LANGS) or "en"
|
||||
|
||||
|
||||
@@ -74,11 +80,13 @@ def create_app(config_class=Config):
|
||||
def load_user(user_id):
|
||||
return db.session.get(User, int(user_id))
|
||||
|
||||
from blueprints.admin import admin_bp
|
||||
from blueprints.auth import auth_bp
|
||||
from blueprints.dashboard import dashboard_bp
|
||||
from blueprints.equipment import equipment_bp
|
||||
from blueprints.sessions import sessions_bp
|
||||
from blueprints.analyses import analyses_bp
|
||||
app.register_blueprint(admin_bp)
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(dashboard_bp)
|
||||
app.register_blueprint(equipment_bp)
|
||||
@@ -116,6 +124,9 @@ def create_app(config_class=Config):
|
||||
def set_lang(lang: str):
|
||||
if lang in SUPPORTED_LANGS:
|
||||
flask_session["lang"] = lang
|
||||
if current_user.is_authenticated:
|
||||
current_user.language = lang
|
||||
db.session.commit()
|
||||
return redirect(request.referrer or "/")
|
||||
|
||||
@app.route("/")
|
||||
|
||||
100
blueprints/admin.py
Normal file
100
blueprints/admin.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for
|
||||
from flask_babel import _
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from extensions import db
|
||||
from models import User
|
||||
|
||||
admin_bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
|
||||
ROLES = ["user", "admin"]
|
||||
|
||||
|
||||
def _require_admin():
|
||||
if not current_user.is_authenticated or current_user.role != "admin":
|
||||
abort(403)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# User list
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@admin_bp.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
_require_admin()
|
||||
users = db.session.scalars(
|
||||
db.select(User).order_by(User.created_at.desc())
|
||||
).all()
|
||||
return render_template("admin/users.html", users=users, roles=ROLES)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Change role
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@admin_bp.route("/users/<int:user_id>/role", methods=["POST"])
|
||||
@login_required
|
||||
def change_role(user_id: int):
|
||||
_require_admin()
|
||||
user = db.session.get(User, user_id)
|
||||
if user is None:
|
||||
abort(404)
|
||||
new_role = request.form.get("role", "user")
|
||||
if new_role not in ROLES:
|
||||
flash(_("Invalid role."), "error")
|
||||
return redirect(url_for("admin.index"))
|
||||
# Prevent removing the last admin
|
||||
if user.role == "admin" and new_role != "admin":
|
||||
admin_count = db.session.scalar(
|
||||
db.select(db.func.count()).select_from(User).where(User.role == "admin")
|
||||
)
|
||||
if admin_count <= 1:
|
||||
flash(_("Cannot remove the last admin."), "error")
|
||||
return redirect(url_for("admin.index"))
|
||||
user.role = new_role
|
||||
db.session.commit()
|
||||
flash(_("Role updated for %(email)s.", email=user.email), "success")
|
||||
return redirect(url_for("admin.index"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reset password
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@admin_bp.route("/users/<int:user_id>/password", methods=["POST"])
|
||||
@login_required
|
||||
def reset_password(user_id: int):
|
||||
_require_admin()
|
||||
user = db.session.get(User, user_id)
|
||||
if user is None:
|
||||
abort(404)
|
||||
new_pw = request.form.get("new_password", "").strip()
|
||||
if len(new_pw) < 8:
|
||||
flash(_("Password must be at least 8 characters."), "error")
|
||||
return redirect(url_for("admin.index"))
|
||||
user.set_password(new_pw)
|
||||
db.session.commit()
|
||||
flash(_("Password reset for %(email)s.", email=user.email), "success")
|
||||
return redirect(url_for("admin.index"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Delete user
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@admin_bp.route("/users/<int:user_id>/delete", methods=["POST"])
|
||||
@login_required
|
||||
def delete_user(user_id: int):
|
||||
_require_admin()
|
||||
if user_id == current_user.id:
|
||||
flash(_("You cannot delete your own account."), "error")
|
||||
return redirect(url_for("admin.index"))
|
||||
user = db.session.get(User, user_id)
|
||||
if user is None:
|
||||
abort(404)
|
||||
email = user.email
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash(_("User %(email)s deleted.", email=email), "success")
|
||||
return redirect(url_for("admin.index"))
|
||||
@@ -12,6 +12,7 @@ from flask import (
|
||||
render_template,
|
||||
request,
|
||||
send_from_directory,
|
||||
session as flask_session,
|
||||
url_for,
|
||||
)
|
||||
from flask_babel import _
|
||||
@@ -28,6 +29,13 @@ auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _login(user: "User") -> None:
|
||||
"""Log in user and restore their language preference into the session."""
|
||||
login_user(user)
|
||||
if user.language:
|
||||
flask_session["lang"] = user.language
|
||||
|
||||
|
||||
def _safe_next() -> str:
|
||||
target = request.args.get("next") or ""
|
||||
if target and urlparse(target).netloc == "":
|
||||
@@ -107,7 +115,7 @@ def login():
|
||||
|
||||
user.last_login_at = datetime.now(timezone.utc)
|
||||
db.session.commit()
|
||||
login_user(user)
|
||||
_login(user)
|
||||
return redirect(_safe_next())
|
||||
|
||||
return render_template("auth/login.html")
|
||||
@@ -167,7 +175,7 @@ def register():
|
||||
_dispatch_confirmation(user)
|
||||
return render_template("auth/confirm_pending.html", email=email)
|
||||
|
||||
login_user(user)
|
||||
_login(user)
|
||||
flash(_("Account created! Welcome."), "success")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
@@ -186,7 +194,7 @@ def confirm_email(token: str):
|
||||
user.email_confirmed = True
|
||||
user.email_confirm_token = None
|
||||
db.session.commit()
|
||||
login_user(user)
|
||||
_login(user)
|
||||
flash(_("Email confirmed! Welcome."), "success")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
@@ -243,7 +251,7 @@ def callback_google():
|
||||
flash(_("This email is already registered with a different login method."), "error")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
login_user(user)
|
||||
_login(user)
|
||||
return redirect(_safe_next())
|
||||
|
||||
|
||||
@@ -293,7 +301,7 @@ def callback_github():
|
||||
flash(_("This email is already registered with a different login method."), "error")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
login_user(user)
|
||||
_login(user)
|
||||
return redirect(_safe_next())
|
||||
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ services:
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- ${NETWORK}
|
||||
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
@@ -30,7 +30,14 @@ services:
|
||||
volumes:
|
||||
- app_storage:/app/storage
|
||||
- .:/app # bind-mount source so code changes are live without a rebuild
|
||||
networks:
|
||||
- ${NETWORK}
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
app_storage:
|
||||
|
||||
|
||||
networks:
|
||||
${NETWORK}:
|
||||
external: true
|
||||
|
||||
44
migrations/versions/6818f37f4124_user_role_and_language.py
Normal file
44
migrations/versions/6818f37f4124_user_role_and_language.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""user_role_and_language
|
||||
|
||||
Revision ID: 6818f37f4124
|
||||
Revises: bf96ceb7f076
|
||||
Create Date: 2026-03-19 15:51:15.091825
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6818f37f4124'
|
||||
down_revision = 'bf96ceb7f076'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('role', sa.String(length=20), nullable=True))
|
||||
batch_op.add_column(sa.Column('language', sa.String(length=10), nullable=True))
|
||||
|
||||
op.execute("UPDATE users SET role = 'user' WHERE role IS NULL")
|
||||
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.alter_column('role', nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.drop_column('language')
|
||||
batch_op.drop_column('role')
|
||||
|
||||
op.execute("UPDATE users SET role = 'user' WHERE role IS NULL")
|
||||
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.alter_column('role', nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -38,6 +38,8 @@ class User(UserMixin, db.Model):
|
||||
email_confirm_token: Mapped[str | None] = mapped_column(String(128))
|
||||
show_equipment_public: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
bio: Mapped[str | None] = mapped_column(Text)
|
||||
role: Mapped[str] = mapped_column(String(20), nullable=False, default="user")
|
||||
language: Mapped[str | None] = mapped_column(String(10))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
|
||||
124
templates/admin/users.html
Normal file
124
templates/admin/users.html
Normal file
@@ -0,0 +1,124 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ _('Admin — Users') }} — The Shooter's Network{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem;">
|
||||
<h1 style="margin:0;">{{ _('User Management') }}</h1>
|
||||
<span style="font-size:0.85rem;color:#888;">{{ users|length }} {{ _('users') }}</span>
|
||||
</div>
|
||||
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="min-width:900px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ _('User') }}</th>
|
||||
<th>{{ _('Provider') }}</th>
|
||||
<th>{{ _('Role') }}</th>
|
||||
<th>{{ _('Language') }}</th>
|
||||
<th>{{ _('Joined') }}</th>
|
||||
<th>{{ _('Last login') }}</th>
|
||||
<th>{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
{# User info #}
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:.6rem;">
|
||||
{% if u.effective_avatar_url %}
|
||||
<img src="{{ u.effective_avatar_url }}" style="width:28px;height:28px;border-radius:50%;object-fit:cover;" alt="">
|
||||
{% else %}
|
||||
<div style="width:28px;height:28px;border-radius:50%;background:#e0e4f0;display:flex;align-items:center;justify-content:center;font-size:.75rem;color:#666;font-weight:700;">
|
||||
{{ (u.display_name or u.email)[0].upper() }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:.9rem;">{{ u.display_name or '—' }}</div>
|
||||
<div style="font-size:.78rem;color:#888;">{{ u.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{# Provider #}
|
||||
<td style="font-size:.82rem;color:#666;">
|
||||
{% if u.provider == 'google' %}🔵 Google
|
||||
{% elif u.provider == 'github' %}⚫ GitHub
|
||||
{% else %}🔑 {{ _('Local') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Role badge + change form #}
|
||||
<td>
|
||||
<form method="post" action="{{ url_for('admin.change_role', user_id=u.id) }}"
|
||||
style="display:flex;gap:.4rem;align-items:center;">
|
||||
<select name="role" style="padding:.2rem .5rem;font-size:.82rem;border:1px solid #ccc;border-radius:4px;background:#fff;">
|
||||
{% for r in roles %}
|
||||
<option value="{{ r }}" {% if u.role == r %}selected{% endif %}>{{ r }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit"
|
||||
style="background:#f0f4ff;color:#1a1a2e;border:1px solid #c8d4f0;border-radius:4px;padding:.2rem .6rem;font-size:.78rem;cursor:pointer;">
|
||||
{{ _('Set') }}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
{# Language #}
|
||||
<td style="font-size:.85rem;color:#666;">
|
||||
{{ u.language or '—' }}
|
||||
</td>
|
||||
|
||||
{# Dates #}
|
||||
<td style="font-size:.8rem;color:#888;white-space:nowrap;">{{ u.created_at.strftime('%d %b %Y') }}</td>
|
||||
<td style="font-size:.8rem;color:#888;white-space:nowrap;">
|
||||
{{ u.last_login_at.strftime('%d %b %Y') if u.last_login_at else '—' }}
|
||||
</td>
|
||||
|
||||
{# Actions #}
|
||||
<td>
|
||||
<div style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;">
|
||||
|
||||
{# Reset password (local accounts only) #}
|
||||
{% if u.provider == 'local' %}
|
||||
<details style="display:inline;">
|
||||
<summary style="display:inline-block;padding:.2rem .6rem;background:#f0f4ff;color:#1a1a2e;
|
||||
border:1px solid #c8d4f0;border-radius:4px;font-size:.78rem;cursor:pointer;list-style:none;">
|
||||
🔑 {{ _('Reset pwd') }}
|
||||
</summary>
|
||||
<form method="post" action="{{ url_for('admin.reset_password', user_id=u.id) }}"
|
||||
style="display:flex;gap:.4rem;align-items:center;margin-top:.35rem;flex-wrap:wrap;">
|
||||
<input type="password" name="new_password" required minlength="8"
|
||||
placeholder="{{ _('New password (min 8)') }}"
|
||||
style="padding:.3rem .6rem;border:1px solid #ccc;border-radius:4px;font-size:.82rem;width:180px;">
|
||||
<button type="submit"
|
||||
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.3rem .7rem;font-size:.78rem;cursor:pointer;">
|
||||
{{ _('Save') }}
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
{# Delete — cannot delete yourself #}
|
||||
{% if u.id != current_user.id %}
|
||||
<form method="post" action="{{ url_for('admin.delete_user', user_id=u.id) }}"
|
||||
onsubmit="return confirm('{{ _('Delete user %(email)s? All their data will be permanently removed.', email=u.email) | e }}');">
|
||||
<button type="submit"
|
||||
style="background:#fff0f0;color:#c0392b;border:1px solid #f5c6c6;border-radius:4px;
|
||||
padding:.2rem .6rem;font-size:.78rem;cursor:pointer;">
|
||||
{{ _('Delete') }}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span style="font-size:.75rem;color:#aaa;">{{ _('(you)') }}</span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -250,6 +250,7 @@
|
||||
<a href="{{ url_for('equipment.index') }}">{{ _('Equipment') }}</a>
|
||||
<a href="{{ url_for('sessions.index') }}">{{ _('Sessions') }}</a>
|
||||
<a href="{{ url_for('dashboard.index') }}">{{ _('Dashboard') }}</a>
|
||||
{% if current_user.role == 'admin' %}<a href="{{ url_for('admin.index') }}" style="color:#f9a825;">{{ _('Admin') }}</a>{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -304,6 +305,7 @@
|
||||
<a href="{{ url_for('equipment.index') }}">{{ _('Equipment') }}</a>
|
||||
<a href="{{ url_for('sessions.index') }}">{{ _('Sessions') }}</a>
|
||||
<a href="{{ url_for('dashboard.index') }}">{{ _('Dashboard') }}</a>
|
||||
{% if current_user.role == 'admin' %}<a href="{{ url_for('admin.index') }}" style="color:#f9a825;">{{ _('Admin') }}</a>{% endif %}
|
||||
<a href="{{ url_for('auth.profile') }}">{{ _('Profile') }}</a>
|
||||
<form method="post" action="{{ url_for('auth.logout') }}" style="padding:0;border:none;">
|
||||
<button type="submit">{{ _('Logout') }}</button>
|
||||
|
||||
Binary file not shown.
@@ -651,3 +651,69 @@ msgstr "Notiz gespeichert."
|
||||
|
||||
msgid "Please log in to access this page."
|
||||
msgstr "Bitte melden Sie sich an, um auf diese Seite zuzugreifen."
|
||||
|
||||
msgid "Admin — Users"
|
||||
msgstr "Admin — Benutzer"
|
||||
|
||||
msgid "User Management"
|
||||
msgstr "Benutzerverwaltung"
|
||||
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
msgid "Provider"
|
||||
msgstr "Anbieter"
|
||||
|
||||
msgid "Role"
|
||||
msgstr "Rolle"
|
||||
|
||||
msgid "Language"
|
||||
msgstr "Sprache"
|
||||
|
||||
msgid "Joined"
|
||||
msgstr "Beigetreten"
|
||||
|
||||
msgid "Last login"
|
||||
msgstr "Letzter Login"
|
||||
|
||||
msgid "Actions"
|
||||
msgstr "Aktionen"
|
||||
|
||||
msgid "Local"
|
||||
msgstr "Lokal"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Setzen"
|
||||
|
||||
msgid "Reset pwd"
|
||||
msgstr "Passwort reset"
|
||||
|
||||
msgid "New password (min 8)"
|
||||
msgstr "Neues Passwort (min 8)"
|
||||
|
||||
msgid "Delete user %(email)s? All their data will be permanently removed."
|
||||
msgstr "Benutzer %(email)s löschen? Alle Daten werden dauerhaft entfernt."
|
||||
|
||||
msgid "(you)"
|
||||
msgstr "(Sie)"
|
||||
|
||||
msgid "Invalid role."
|
||||
msgstr "Ungültige Rolle."
|
||||
|
||||
msgid "Cannot remove the last admin."
|
||||
msgstr "Der letzte Administrator kann nicht entfernt werden."
|
||||
|
||||
msgid "Role updated for %(email)s."
|
||||
msgstr "Rolle für %(email)s aktualisiert."
|
||||
|
||||
msgid "Password reset for %(email)s."
|
||||
msgstr "Passwort für %(email)s zurückgesetzt."
|
||||
|
||||
msgid "You cannot delete your own account."
|
||||
msgstr "Sie können Ihr eigenes Konto nicht löschen."
|
||||
|
||||
msgid "User %(email)s deleted."
|
||||
msgstr "Benutzer %(email)s gelöscht."
|
||||
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -651,3 +651,69 @@ msgstr "Note sauvegardée."
|
||||
|
||||
msgid "Please log in to access this page."
|
||||
msgstr "Veuillez vous connecter pour accéder à cette page."
|
||||
|
||||
msgid "Admin — Users"
|
||||
msgstr "Admin — Utilisateurs"
|
||||
|
||||
msgid "User Management"
|
||||
msgstr "Gestion des utilisateurs"
|
||||
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
msgid "Provider"
|
||||
msgstr "Fournisseur"
|
||||
|
||||
msgid "Role"
|
||||
msgstr "Rôle"
|
||||
|
||||
msgid "Language"
|
||||
msgstr "Langue"
|
||||
|
||||
msgid "Joined"
|
||||
msgstr "Inscrit"
|
||||
|
||||
msgid "Last login"
|
||||
msgstr "Dernière connexion"
|
||||
|
||||
msgid "Actions"
|
||||
msgstr "Actions"
|
||||
|
||||
msgid "Local"
|
||||
msgstr "Local"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Définir"
|
||||
|
||||
msgid "Reset pwd"
|
||||
msgstr "Réinit. mdp"
|
||||
|
||||
msgid "New password (min 8)"
|
||||
msgstr "Nouveau mot de passe (min 8)"
|
||||
|
||||
msgid "Delete user %(email)s? All their data will be permanently removed."
|
||||
msgstr "Supprimer l'utilisateur %(email)s ? Toutes ses données seront définitivement supprimées."
|
||||
|
||||
msgid "(you)"
|
||||
msgstr "(vous)"
|
||||
|
||||
msgid "Invalid role."
|
||||
msgstr "Rôle invalide."
|
||||
|
||||
msgid "Cannot remove the last admin."
|
||||
msgstr "Impossible de supprimer le dernier administrateur."
|
||||
|
||||
msgid "Role updated for %(email)s."
|
||||
msgstr "Rôle mis à jour pour %(email)s."
|
||||
|
||||
msgid "Password reset for %(email)s."
|
||||
msgstr "Mot de passe réinitialisé pour %(email)s."
|
||||
|
||||
msgid "You cannot delete your own account."
|
||||
msgstr "Vous ne pouvez pas supprimer votre propre compte."
|
||||
|
||||
msgid "User %(email)s deleted."
|
||||
msgstr "Utilisateur %(email)s supprimé."
|
||||
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
Reference in New Issue
Block a user