change user_name still used as login and add display_name for dashboard view instead of username on login

This commit is contained in:
Alberto-Audrix committed 2026-09-10 10:51:51 +07:00
1 parent 7a562c18ec
commit 11b0ca26a5
33 files changed
+180 -145

No files matched your search

@@ -0,0 +1,34 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0012_user_login"),
]
operations = [
migrations.RenameField(
model_name="user",
old_name="user_name",
new_name="display_name",
),
migrations.RenameField(
model_name="user",
old_name="user_login",
new_name="user_name",
),
migrations.AlterModelOptions(
name="user",
options={"ordering": ["display_name"]},
),
migrations.AlterField(
model_name="user",
name="display_name",
field=models.CharField(max_length=30, unique=True),
),
migrations.AlterField(
model_name="user",
name="user_name",
field=models.CharField(max_length=30, unique=True),
),
]
+12 -12
View File
@@ -9,22 +9,22 @@ from django.utils import timezone
class UserManager(BaseUserManager):
def create_user(self, user_login: str, password: str | None = None, **extra_fields):
if not user_login:
raise ValueError("user_login is required")
def create_user(self, user_name: str, password: str | None = None, **extra_fields):
if not user_name:
raise ValueError("user_name is required")
extra_fields.setdefault("status", User.STATUS_GM)
extra_fields.setdefault("is_superuser", False)
extra_fields.setdefault("user_name", user_login)
user = self.model(user_login=user_login, **extra_fields)
extra_fields.setdefault("display_name", user_name)
user = self.model(user_name=user_name, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, user_login: str, password: str | None = None, **extra_fields):
def create_superuser(self, user_name: str, password: str | None = None, **extra_fields):
"""Break-glass: status=superuser (ops + user management). Hidden from user lists."""
extra_fields["status"] = User.STATUS_SUPERUSER
extra_fields["is_superuser"] = True
return self.create_user(user_login, password, **extra_fields)
return self.create_user(user_name, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin):
@@ -76,8 +76,8 @@ class User(AbstractBaseUser, PermissionsMixin):
)
user_id = models.BigAutoField(primary_key=True)
user_login = models.CharField(max_length=30, unique=True)
user_name = models.CharField(max_length=30, unique=True)
display_name = models.CharField(max_length=30, unique=True)
status = models.CharField(max_length=30, choices=STATUS_CHOICES, default=STATUS_GM)
# BUH who manages this GM / GM Admin (unused for director / buh themselves).
managed_by = models.ForeignKey(
@@ -104,15 +104,15 @@ class User(AbstractBaseUser, PermissionsMixin):
objects = UserManager()
USERNAME_FIELD = "user_login"
REQUIRED_FIELDS: list[str] = ["user_name"]
USERNAME_FIELD = "user_name"
REQUIRED_FIELDS: list[str] = ["display_name"]
class Meta:
db_table = "user_access"
ordering = ["user_name"]
ordering = ["display_name"]
def __str__(self) -> str:
return self.user_name
return self.display_name
@property
def is_executive(self) -> bool:
+9 -9
View File
@@ -19,19 +19,19 @@ class UserSerializer(PkAsIdMixin, serializers.ModelSerializer):
required=False,
)
managed_by_name = serializers.CharField(
source="managed_by.user_name", read_only=True, allow_null=True
source="managed_by.display_name", read_only=True, allow_null=True
)
managed_by_pending = serializers.PrimaryKeyRelatedField(read_only=True)
managed_by_pending_name = serializers.CharField(
source="managed_by_pending.user_name", read_only=True, allow_null=True
source="managed_by_pending.display_name", read_only=True, allow_null=True
)
class Meta:
model = User
fields = [
"id",
"user_login",
"user_name",
"display_name",
"status",
"is_superuser",
"managed_by",
@@ -64,19 +64,19 @@ class UserWriteSerializer(PkAsIdMixin, serializers.ModelSerializer):
required=False,
)
managed_by_name = serializers.CharField(
source="managed_by.user_name", read_only=True, allow_null=True
source="managed_by.display_name", read_only=True, allow_null=True
)
managed_by_pending = serializers.PrimaryKeyRelatedField(read_only=True)
managed_by_pending_name = serializers.CharField(
source="managed_by_pending.user_name", read_only=True, allow_null=True
source="managed_by_pending.display_name", read_only=True, allow_null=True
)
class Meta:
model = User
fields = [
"id",
"user_login",
"user_name",
"display_name",
"status",
"is_superuser",
"password",
@@ -136,9 +136,9 @@ class UserWriteSerializer(PkAsIdMixin, serializers.ModelSerializer):
def create(self, validated_data):
password = validated_data.pop("password")
user_login = validated_data.pop("user_login")
user_name = validated_data.pop("user_name")
return User.objects.create_user(
user_login=user_login, password=password, **validated_data
user_name=user_name, password=password, **validated_data
)
def update(self, instance, validated_data):
@@ -161,7 +161,7 @@ class SetPasswordSerializer(serializers.Serializer):
class LoginSerializer(serializers.Serializer):
user_login = serializers.CharField(max_length=30)
user_name = serializers.CharField(max_length=30)
password = serializers.CharField(write_only=True)
+19 -19
View File
@@ -8,7 +8,7 @@ from apps.accounts.models import User
class AccountAdminModelTests(TestCase):
def test_account_admin_flags_and_executive(self):
user = User.objects.create_user(
user_login="acct1",
user_name="acct1",
password="secret",
status=User.STATUS_ACCOUNT_ADMIN,
)
@@ -18,7 +18,7 @@ class AccountAdminModelTests(TestCase):
def test_inactive_clears_flags(self):
user = User.objects.create_user(
user_login="acct2",
user_name="acct2",
password="secret",
status=User.STATUS_ACCOUNT_ADMIN,
)
@@ -30,7 +30,7 @@ class AccountAdminModelTests(TestCase):
def test_director_admin_is_not_superuser(self):
user = User.objects.create_user(
user_login="diradmin1",
user_name="diradmin1",
password="secret",
status=User.STATUS_DIRECTOR_ADMIN,
)
@@ -42,21 +42,21 @@ class AccountAdminApiTests(TestCase):
def setUp(self):
self.client = APIClient()
self.account_admin = User.objects.create_user(
user_login="acctadmin",
user_name="acctadmin",
password="acct123",
status=User.STATUS_ACCOUNT_ADMIN,
)
self.gm = User.objects.create_user(
user_login="gm1", password="gm123", status=User.STATUS_GM
user_name="gm1", password="gm123", status=User.STATUS_GM
)
self.buh = User.objects.create_user(
user_login="buh1", password="buh123", status=User.STATUS_BUH
user_name="buh1", password="buh123", status=User.STATUS_BUH
)
self.director = User.objects.create_user(
user_login="dir1", password="dir123", status=User.STATUS_DIRECTOR
user_name="dir1", password="dir123", status=User.STATUS_DIRECTOR
)
self.superuser = User.objects.create_superuser(
user_login="admin1",
user_name="admin1",
password="admin123",
)
self.assertTrue(self.superuser.is_superuser)
@@ -67,15 +67,15 @@ class AccountAdminApiTests(TestCase):
response = self.client.post(
"/api/v1/users/",
{
"user_login": "newgm",
"user_name": "newgm",
"display_name": "newgm",
"status": User.STATUS_GM,
"password": "temp-pass",
},
format="json",
)
self.assertEqual(response.status_code, 201, response.data)
created = User.objects.get(user_name="newgm")
created = User.objects.get(display_name="newgm")
self.assertEqual(created.status, User.STATUS_GM)
self.assertIsNotNone(
authenticate(username="newgm", password="temp-pass")
@@ -85,7 +85,7 @@ class AccountAdminApiTests(TestCase):
self.client.force_authenticate(self.account_admin)
response = self.client.post(
"/api/v1/users/",
{"user_login": "nopass", "user_name": "nopass", "status": User.STATUS_GM},
{"user_name": "nopass", "display_name": "nopass", "status": User.STATUS_GM},
format="json",
)
self.assertEqual(response.status_code, 400)
@@ -96,8 +96,8 @@ class AccountAdminApiTests(TestCase):
response = self.client.post(
"/api/v1/users/",
{
"user_login": "blocked",
"user_name": "blocked",
"display_name": "blocked",
"status": User.STATUS_GM,
"password": "x",
},
@@ -109,7 +109,7 @@ class AccountAdminApiTests(TestCase):
self.client.force_authenticate(self.account_admin)
response = self.client.get("/api/v1/users/")
self.assertEqual(response.status_code, 200)
names = {row["user_name"] for row in response.data["results"]}
names = {row["display_name"] for row in response.data["results"]}
self.assertIn("gm1", names)
self.assertIn("buh1", names)
self.assertIn("dir1", names)
@@ -148,15 +148,15 @@ class AccountAdminApiTests(TestCase):
{"password": "hack"},
format="json",
)
self.assertEqual(response.status_code, 403, actor.user_name)
self.assertEqual(response.status_code, 403, actor.display_name)
def test_superuser_break_glass_create(self):
self.client.force_authenticate(self.superuser)
response = self.client.post(
"/api/v1/users/",
{
"user_login": "breakglass",
"user_name": "breakglass",
"display_name": "breakglass",
"status": User.STATUS_BUH,
"password": "bg-pass",
},
@@ -191,7 +191,7 @@ class AccountAdminApiTests(TestCase):
def test_peer_account_admin_can_deactivate_other_account_admin(self):
peer = User.objects.create_user(
user_login="acctadmin2",
user_name="acctadmin2",
password="acct123",
status=User.STATUS_ACCOUNT_ADMIN,
)
@@ -207,11 +207,11 @@ class AccountAdminApiTests(TestCase):
self.assertFalse(peer.is_active)
def test_superuser_hidden_from_user_list(self):
User.objects.create_superuser(user_login="hidden_su", password="x")
User.objects.create_superuser(user_name="hidden_su", password="x")
self.client.force_authenticate(self.account_admin)
response = self.client.get("/api/v1/users/")
self.assertEqual(response.status_code, 200)
names = {row["user_name"] for row in response.data["results"]}
names = {row["display_name"] for row in response.data["results"]}
self.assertNotIn("hidden_su", names)
self.assertNotIn("admin1", names)
@@ -220,8 +220,8 @@ class AccountAdminApiTests(TestCase):
response = self.client.post(
"/api/v1/users/",
{
"user_login": "bad_su",
"user_name": "bad_su",
"display_name": "bad_su",
"status": User.STATUS_SUPERUSER,
"password": "x",
},
+10 -10
View File
@@ -8,13 +8,13 @@ class ManagedByRequestTests(TestCase):
def setUp(self):
self.client = APIClient()
self.buh = User.objects.create_user(
user_login="buh1", password="x", status=User.STATUS_BUH
user_name="buh1", password="x", status=User.STATUS_BUH
)
self.buh_other = User.objects.create_user(
user_login="buh2", password="x", status=User.STATUS_BUH
user_name="buh2", password="x", status=User.STATUS_BUH
)
self.gm = User.objects.create_user(
user_login="gm1", password="x", status=User.STATUS_GM
user_name="gm1", password="x", status=User.STATUS_GM
)
def test_gm_requests_and_buh_approves(self):
@@ -30,7 +30,7 @@ class ManagedByRequestTests(TestCase):
self.client.force_authenticate(self.buh)
pending = self.client.get("/api/v1/users/pending-managed/")
self.assertEqual(pending.status_code, 200)
self.assertEqual({row["user_name"] for row in pending.data}, {"gm1"})
self.assertEqual({row["display_name"] for row in pending.data}, {"gm1"})
approve = self.client.post(f"/api/v1/users/{self.gm.pk}/approve-managed-by/", {})
self.assertEqual(approve.status_code, 200)
@@ -39,7 +39,7 @@ class ManagedByRequestTests(TestCase):
self.assertIsNone(self.gm.managed_by_pending_id)
managed = self.client.get("/api/v1/users/")
self.assertEqual({row["user_name"] for row in managed.data["results"]}, {"gm1"})
self.assertEqual({row["display_name"] for row in managed.data["results"]}, {"gm1"})
def test_other_buh_cannot_approve(self):
self.gm.request_managed_by(self.buh)
@@ -60,17 +60,17 @@ class ManagedByRequestTests(TestCase):
def test_gm_lists_buhs_only(self):
User.objects.create_user(
user_login="buadmin1", password="x", status=User.STATUS_BU_ADMIN
user_name="buadmin1", password="x", status=User.STATUS_BU_ADMIN
)
self.client.force_authenticate(self.gm)
response = self.client.get("/api/v1/users/buhs/")
self.assertEqual(response.status_code, 200)
names = {row["user_name"] for row in response.data}
names = {row["display_name"] for row in response.data}
self.assertEqual(names, {"buh1", "buh2"})
def test_gm_admin_cannot_request_buh(self):
gm_admin = User.objects.create_user(
user_login="gma1", password="x", status=User.STATUS_GM_ADMIN
user_name="gma1", password="x", status=User.STATUS_GM_ADMIN
)
self.client.force_authenticate(gm_admin)
response = self.client.post(
@@ -80,7 +80,7 @@ class ManagedByRequestTests(TestCase):
def test_gm_cannot_request_bu_admin(self):
bu_admin = User.objects.create_user(
user_login="buadmin1", password="x", status=User.STATUS_BU_ADMIN
user_name="buadmin1", password="x", status=User.STATUS_BU_ADMIN
)
self.client.force_authenticate(self.gm)
response = self.client.post(
@@ -90,7 +90,7 @@ class ManagedByRequestTests(TestCase):
def test_bu_admin_cannot_approve(self):
bu_admin = User.objects.create_user(
user_login="buadmin1", password="x", status=User.STATUS_BU_ADMIN
user_name="buadmin1", password="x", status=User.STATUS_BU_ADMIN
)
self.gm.managed_by_pending = bu_admin
self.gm.save(update_fields=["managed_by_pending", "updated_at"])
+6 -6
View File
@@ -10,19 +10,19 @@ class VisibilityScopeTests(TestCase):
def setUp(self):
self.client = APIClient()
self.director = User.objects.create_user(
user_login="dir1", password="x", status=User.STATUS_DIRECTOR
user_name="dir1", password="x", status=User.STATUS_DIRECTOR
)
self.buh = User.objects.create_user(
user_login="buh1", password="x", status=User.STATUS_BUH
user_name="buh1", password="x", status=User.STATUS_BUH
)
self.buh_other = User.objects.create_user(
user_login="buh2", password="x", status=User.STATUS_BUH
user_name="buh2", password="x", status=User.STATUS_BUH
)
self.gm = User.objects.create_user(
user_login="gm1", password="x", status=User.STATUS_GM, managed_by=self.buh
user_name="gm1", password="x", status=User.STATUS_GM, managed_by=self.buh
)
self.gm_other = User.objects.create_user(
user_login="gm2",
user_name="gm2",
password="x",
status=User.STATUS_GM,
managed_by=self.buh_other,
@@ -67,7 +67,7 @@ class VisibilityScopeTests(TestCase):
self.client.force_authenticate(self.buh)
response = self.client.get("/api/v1/users/")
self.assertEqual(response.status_code, 200)
names = {row["user_name"] for row in response.data["results"]}
names = {row["display_name"] for row in response.data["results"]}
self.assertEqual(names, {"gm1"})
def test_gm_sees_own_city_sites_only(self):
+12 -12
View File
@@ -5,11 +5,11 @@ from apps.accounts.models import User
class UserLoginFieldTests(TestCase):
def test_authenticate_uses_user_login_not_user_name(self):
def test_authenticate_uses_user_name_not_display_name(self):
User.objects.create_user(
user_login="gm.login",
user_name="gm.login",
password="gm123",
user_name="gm",
display_name="gm",
status=User.STATUS_GM,
)
self.assertIsNotNone(
@@ -17,18 +17,18 @@ class UserLoginFieldTests(TestCase):
)
self.assertIsNone(authenticate(username="gm", password="gm123"))
def test_create_user_defaults_user_name_to_login(self):
user = User.objects.create_user(user_login="solo", password="x")
self.assertEqual(user.user_login, "solo")
def test_create_user_defaults_display_name_to_user_name(self):
user = User.objects.create_user(user_name="solo", password="x")
self.assertEqual(user.user_name, "solo")
self.assertEqual(user.display_name, "solo")
class LoginApiUserLoginTests(TestCase):
def test_login_accepts_user_login(self):
def test_login_accepts_user_name(self):
User.objects.create_user(
user_login="gm.login",
user_name="gm.login",
password="gm123",
user_name="gm",
display_name="gm",
status=User.STATUS_GM,
)
client = Client()
@@ -36,10 +36,10 @@ class LoginApiUserLoginTests(TestCase):
token = client.cookies["csrftoken"].value
response = client.post(
"/api/v1/auth/login/",
{"user_login": "gm.login", "password": "gm123"},
{"user_name": "gm.login", "password": "gm123"},
content_type="application/json",
HTTP_X_CSRFTOKEN=token,
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["user_login"], "gm.login")
self.assertEqual(response.json()["user_name"], "gm")
self.assertEqual(response.json()["user_name"], "gm.login")
self.assertEqual(response.json()["display_name"], "gm")
+4 -4
View File
@@ -57,7 +57,7 @@ class UserViewSet(viewsets.ModelViewSet):
def get_queryset(self):
qs = User.objects.select_related("managed_by", "managed_by_pending").order_by(
"user_name"
"display_name"
)
return filter_users_queryset(qs, self.request.user)
@@ -103,7 +103,7 @@ class UserViewSet(viewsets.ModelViewSet):
rows = User.objects.filter(
status=User.STATUS_BUH,
is_active=True,
).order_by("user_name")
).order_by("display_name")
return Response(UserSerializer(rows, many=True).data)
@action(detail=False, methods=["get"], url_path="pending-managed")
@@ -114,7 +114,7 @@ class UserViewSet(viewsets.ModelViewSet):
rows = User.objects.filter(
managed_by_pending=request.user,
status=User.STATUS_GM,
).select_related("managed_by", "managed_by_pending").order_by("user_name")
).select_related("managed_by", "managed_by_pending").order_by("display_name")
return Response(UserSerializer(rows, many=True).data)
@action(detail=False, methods=["post"], url_path="request-managed-by")
@@ -235,7 +235,7 @@ class LoginView(APIView):
serializer.is_valid(raise_exception=True)
user = authenticate(
request,
username=serializer.validated_data["user_login"],
username=serializer.validated_data["user_name"],
password=serializer.validated_data["password"],
)
if user is None:
+1 -1
View File
@@ -5,7 +5,7 @@ from apps.farms.models import Cycle, Flock, Kandang, Site
class SiteSerializer(PkAsIdMixin, serializers.ModelSerializer):
owner_name = serializers.CharField(source="user.user_name", read_only=True)
owner_name = serializers.CharField(source="user.display_name", read_only=True)
class Meta:
model = Site
+5 -5
View File
@@ -23,9 +23,9 @@ def _gm_visible_site(*, owner: User, site_name: str, code: str) -> Site:
class FarmAPITests(TestCase):
def setUp(self):
self.user = User.objects.create_user(user_login="tester", password="secret")
self.user = User.objects.create_user(user_name="tester", password="secret")
self.admin = User.objects.create_user(
user_login="admin",
user_name="admin",
password="secret",
status=User.STATUS_DIRECTOR_ADMIN,
)
@@ -124,7 +124,7 @@ class FarmAPITests(TestCase):
self.assertEqual(cycle.status, Cycle.STATUS_ACTIVE)
def test_gm_can_approve_cycle_close(self):
gm = User.objects.create_user(user_login="gm_approver", password="x", status=User.STATUS_GM)
gm = User.objects.create_user(user_name="gm_approver", password="x", status=User.STATUS_GM)
site = _gm_visible_site(owner=gm, site_name="GM Site", code="gm-approve")
kandang = Kandang.objects.create(kandang_name="Kandang GM", site=site)
self.client.force_authenticate(gm)
@@ -154,7 +154,7 @@ class FarmAPITests(TestCase):
self.assertEqual(cycle.end_date, proposed)
def test_gm_can_reject_cycle_close(self):
gm = User.objects.create_user(user_login="gm_rejector", password="x", status=User.STATUS_GM)
gm = User.objects.create_user(user_name="gm_rejector", password="x", status=User.STATUS_GM)
site = _gm_visible_site(owner=gm, site_name="GM Reject Site", code="gm-reject")
kandang = Kandang.objects.create(kandang_name="Kandang R", site=site)
self.client.force_authenticate(gm)
@@ -181,7 +181,7 @@ class FarmAPITests(TestCase):
def test_gm_admin_cannot_approve_cycle_close(self):
gm_admin = User.objects.create_user(
user_login="gm_admin_user", password="x", status=User.STATUS_GM_ADMIN
user_name="gm_admin_user", password="x", status=User.STATUS_GM_ADMIN
)
site = _gm_visible_site(
owner=gm_admin, site_name="GM Admin Site", code="gm-admin-site"
+5 -5
View File
@@ -6,14 +6,14 @@ from apps.farms.models import Kandang, Site
class KandangFeedInButtonUrlTests(TestCase):
def setUp(self):
self.owner = User.objects.create_user(user_login="owner", password="secret")
self.owner = User.objects.create_user(user_name="owner", password="secret")
self.admin = User.objects.create_user(
user_login="admin",
user_name="admin",
password="admin123",
status=User.STATUS_DIRECTOR_ADMIN,
)
self.staff = User.objects.create_user(
user_login="staff",
user_name="staff",
password="staff123",
status=User.STATUS_GM,
)
@@ -27,11 +27,11 @@ class KandangFeedInButtonUrlTests(TestCase):
token = client.cookies["csrftoken"].value
return {"HTTP_X_CSRFTOKEN": token}
def _login(self, client: Client, user_login: str, password: str) -> None:
def _login(self, client: Client, user_name: str, password: str) -> None:
client.get("/api/v1/auth/csrf/")
response = client.post(
"/api/v1/auth/login/",
{"user_login": user_login, "password": password},
{"user_name": user_name, "password": password},
content_type="application/json",
**self._csrf_headers(client),
)
@@ -7,7 +7,7 @@ from apps.accounts.models import ApiKey, User
def ensure_bootstrap_user(
*,
user_login: str,
user_name: str,
password: str,
status: str,
is_superuser: bool,
@@ -15,9 +15,9 @@ def ensure_bootstrap_user(
stdout,
) -> User:
user, created = User.objects.get_or_create(
user_login=user_login,
user_name=user_name,
defaults={
"user_name": user_login,
"display_name": user_name,
"status": status,
"is_superuser": is_superuser,
},
@@ -25,7 +25,7 @@ def ensure_bootstrap_user(
if not created and user.status != status:
stdout.write(
f"Updating {user_login!r} status {user.status!r} → {status!r}"
f"Updating {user_name!r} status {user.status!r} → {status!r}"
)
should_set_password = created or not user.has_usable_password() or force_password
@@ -35,12 +35,12 @@ def ensure_bootstrap_user(
user.is_superuser = is_superuser
user.save()
action = "Created" if created else "Updated password for"
stdout.write(f"{action} {status} user {user_login}")
stdout.write(f"{action} {status} user {user_name}")
else:
user.status = status
user.is_superuser = is_superuser
user.save(update_fields=["status", "is_superuser", "updated_at"])
stdout.write(f"User {user_login} already exists (password unchanged)")
stdout.write(f"User {user_name} already exists (password unchanged)")
return user
@@ -71,7 +71,7 @@ class Command(BaseCommand):
)
admin = ensure_bootstrap_user(
user_login=admin_user_name,
user_name=admin_user_name,
password=admin_password,
status=User.STATUS_SUPERUSER,
is_superuser=True,
@@ -91,7 +91,7 @@ class Command(BaseCommand):
"BOOTSTRAP_STAFF_PASSWORD is required when BOOTSTRAP_STAFF_USER is set."
)
ensure_bootstrap_user(
user_login=staff_user_name,
user_name=staff_user_name,
password=staff_password,
status=User.STATUS_GM,
is_superuser=False,
@@ -75,10 +75,10 @@ class Command(BaseCommand):
defaults["managed_by"] = managed_by
user, created = User.objects.get_or_create(
user_login=spec["user_name"],
user_name=spec["user_name"],
defaults={
**defaults,
"user_name": spec["user_name"],
"display_name": spec["user_name"],
},
)
user.status = spec["status"]
@@ -93,7 +93,7 @@ class Command(BaseCommand):
role = spec["status"]
if user.is_superuser:
role = f"{role}+superuser"
link = f", managed_by={managed_by.user_name}" if managed_by else ""
link = f", managed_by={managed_by.display_name}" if managed_by else ""
action = "Created" if created else "Updated"
self.stdout.write(
f"{action} user {spec['user_name']} / {spec['password']} ({role}{link})"
@@ -102,8 +102,8 @@ class Command(BaseCommand):
def _remove_legacy_staff(self, gm: User) -> None:
staff = (
User.objects.filter(user_login="staff").first()
or User.objects.filter(user_name="staff").first()
User.objects.filter(user_name="staff").first()
or User.objects.filter(display_name="staff").first()
)
if not staff:
return
@@ -186,7 +186,7 @@ class Command(BaseCommand):
if site.user_id != user.pk:
site.user = user
site.save(update_fields=["user", "updated_at"])
self.stdout.write(f"Sukawarna site owner: {user.user_name} ({user.status})")
self.stdout.write(f"Sukawarna site owner: {user.display_name} ({user.status})")
kandang, _ = Kandang.objects.get_or_create(
kandang_name="Kandang 1",
@@ -73,7 +73,7 @@ class Command(BaseCommand):
if site.user_id != user.pk:
site.user = user
site.save(update_fields=["user", "updated_at"])
self.stdout.write(f"Sukawarna site owner: {user.user_name} ({user.status})")
self.stdout.write(f"Sukawarna site owner: {user.display_name} ({user.status})")
kandang, _ = Kandang.objects.get_or_create(
kandang_name="Kandang 2",
+4 -4
View File
@@ -15,8 +15,8 @@ from django.core.management import call_command
class BootstrapAdminTests(TestCase):
def test_creates_break_glass_superuser_without_demo_data(self):
call_command("bootstrap_admin")
user = User.objects.get(user_login="admin")
self.assertEqual(user.user_name, "admin")
user = User.objects.get(user_name="admin")
self.assertEqual(user.display_name, "admin")
self.assertEqual(user.status, User.STATUS_SUPERUSER)
self.assertTrue(user.is_superuser)
self.assertTrue(user.check_password("Pr04dm1n"))
@@ -34,8 +34,8 @@ class BootstrapAdminTests(TestCase):
)
def test_creates_gm_when_configured(self):
call_command("bootstrap_admin")
staff = User.objects.get(user_login="staff")
self.assertEqual(staff.user_name, "staff")
staff = User.objects.get(user_name="staff")
self.assertEqual(staff.display_name, "staff")
self.assertEqual(staff.status, User.STATUS_GM)
self.assertFalse(staff.is_superuser)
self.assertTrue(staff.check_password("St4ff!"))
+2 -2
View File
@@ -12,7 +12,7 @@ from apps.operations.models import ChickenWeight, FeedSacks, IotPanel
@override_settings(DASHBOARD_PUBLISH_HOUR=0)
class OperationsAPITests(TestCase):
def setUp(self):
self.user = User.objects.create_user(user_login="tester", password="secret")
self.user = User.objects.create_user(user_name="tester", password="secret")
self.client = APIClient()
self.client.force_authenticate(self.user)
site = Site.objects.create(site_name="Sukawarna", user=self.user)
@@ -311,7 +311,7 @@ class OperationsAPITests(TestCase):
@override_settings(DASHBOARD_PUBLISH_HOUR=17, TIME_ZONE="Asia/Jakarta")
class DashboardVisibilityTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(user_login="tester", password="secret")
self.user = User.objects.create_user(user_name="tester", password="secret")
self.client = APIClient()
self.client.force_authenticate(self.user)
site = Site.objects.create(site_name="Sukawarna", user=self.user)
@@ -62,7 +62,7 @@ class AggregationTests(TestCase):
)
class SyncChickenCountingTests(TestCase):
def setUp(self):
user = User.objects.create_user(user_login="sync-user", password="secret")
user = User.objects.create_user(user_name="sync-user", password="secret")
site = Site.objects.create(site_name="Sukawarna", user=user)
self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=site)
self.cycle = Cycle.objects.create(
@@ -130,7 +130,7 @@ class SyncChickenCountingTests(TestCase):
)
class ChickenCountingSyncApiTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(user_login="tester", password="secret")
self.user = User.objects.create_user(user_name="tester", password="secret")
self.client = APIClient()
self.client.force_authenticate(self.user)
site = Site.objects.create(site_name="Sukawarna", user=self.user)
+1 -1
View File
@@ -156,7 +156,7 @@ class KarungWebCombinedTests(TestCase):
@override_settings(KARUNG_WEB_ADMIN_BASE_URL="http://karung.internal")
class RequestKarungTests(TestCase):
def setUp(self):
user = User.objects.create_user(user_login="tester", password="secret")
user = User.objects.create_user(user_name="tester", password="secret")
site = Site.objects.create(site_name="Sukawarna", user=user)
self.kandang1 = Kandang.objects.create(kandang_name="Kandang 1", site=site)
self.kandang2 = Kandang.objects.create(kandang_name="Kandang 2", site=site)
+1 -1
View File
@@ -8,7 +8,7 @@ class CitySiteSerializer(serializers.ModelSerializer):
# Never echo the outbound city API key back to the browser by default.
has_api_key = serializers.SerializerMethodField()
registered_by_name = serializers.CharField(
source="registered_by.user_name", read_only=True, allow_null=True
source="registered_by.display_name", read_only=True, allow_null=True
)
class Meta:
@@ -57,6 +57,7 @@ erDiagram
USER_ACCESS {
int user_id PK "NOT NULL"
varchar user_name "30 NOT NULL"
varchar display_name "30 NOT NULL"
varchar password_hash "255 NOT NULL"
varchar status "30 NOT NULL"
timestamp created_at "NOT NULL"
+1 -1
View File
@@ -25,10 +25,10 @@
CREATE TABLE IF NOT EXISTS user_access (
user_id SERIAL PRIMARY KEY,
user_name VARCHAR(30) NOT NULL UNIQUE,
display_name VARCHAR(30) NOT NULL UNIQUE,
password VARCHAR(128) NOT NULL,
last_login TIMESTAMP WITH TIME ZONE,
is_superuser BOOLEAN NOT NULL DEFAULT FALSE,
is_staff BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
+1 -1
View File
@@ -28,7 +28,7 @@ const AccountAdminShell: React.FC = () => {
</nav>
<div className="border-t border-gray-200 p-4 space-y-2">
<p className="text-xs text-gray-500 truncate">
{user?.user_name} · {roleLabel}
{user?.display_name} · {roleLabel}
</p>
<button
type="button"
+3 -3
View File
@@ -181,10 +181,10 @@ const Header: React.FC<HeaderProps> = ({
className="flex items-center space-x-2 focus:outline-none"
>
<div className="w-10 h-10 rounded-full bg-red-100 text-red-700 flex items-center justify-center font-bold">
{user.user_name.slice(0, 1).toUpperCase()}
{user.display_name.slice(0, 1).toUpperCase()}
</div>
<div className="hidden lg:block text-left">
<p className="text-sm font-semibold text-gray-800">{user.user_name}</p>
<p className="text-sm font-semibold text-gray-800">{user.display_name}</p>
<p className="text-xs text-gray-500">{ROLE_LABELS[user.status]}</p>
</div>
<i className="fa-solid fa-chevron-down text-xs text-gray-500" />
@@ -192,7 +192,7 @@ const Header: React.FC<HeaderProps> = ({
{open && (
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 z-50">
<div className="px-4 py-3 border-b">
<p className="text-sm font-semibold text-gray-800">{user.user_name}</p>
<p className="text-sm font-semibold text-gray-800">{user.display_name}</p>
<p className="text-xs text-gray-500">{ROLE_LABELS[user.status]}</p>
</div>
<button
+3 -3
View File
@@ -134,7 +134,7 @@ const BuhViewList: React.FC = () => {
className="py-3 flex flex-wrap items-center justify-between gap-3"
>
<div>
<p className="text-sm font-semibold text-gray-800">{gm.user_name}</p>
<p className="text-sm font-semibold text-gray-800">{gm.display_name}</p>
<p className="text-xs text-gray-500">{ROLE_LABELS[gm.status] ?? gm.status}</p>
</div>
<div className="flex gap-2">
@@ -179,7 +179,7 @@ const BuhViewList: React.FC = () => {
return (
<li key={gm.id} className="border border-gray-200 rounded-lg p-4">
<div className="flex flex-wrap items-baseline justify-between gap-2 mb-2">
<p className="text-sm font-semibold text-gray-800">{gm.user_name}</p>
<p className="text-sm font-semibold text-gray-800">{gm.display_name}</p>
<p className="text-xs text-gray-500">
{ROLE_LABELS[gm.status] ?? gm.status} · {sites.length} site
</p>
@@ -361,7 +361,7 @@ const GmViewList: React.FC = () => {
<option value="">— pilih —</option>
{buhs.map((buh) => (
<option key={buh.id} value={buh.id}>
{buh.user_name}
{buh.display_name}
</option>
))}
</select>
+6 -6
View File
@@ -81,8 +81,8 @@ const UserManagementPage: React.FC = () => {
setSaving(true);
try {
await api.users.create({
user_login: userLogin.trim(),
user_name: userName.trim(),
user_name: userLogin.trim(),
display_name: userName.trim(),
status,
password,
});
@@ -115,12 +115,12 @@ const UserManagementPage: React.FC = () => {
};
const onDeactivate = (user: User) => {
if (!window.confirm(`Nonaktifkan akun "${user.user_name}"?`)) return;
if (!window.confirm(`Nonaktifkan akun "${user.display_name}"?`)) return;
void wrap(async () => {
setBusyId(user.id);
try {
await api.users.update(user.id, { status: 'inactive' });
setNotice(`Akun "${user.user_name}" dinonaktifkan.`);
setNotice(`Akun "${user.display_name}" dinonaktifkan.`);
await reload();
} finally {
setBusyId(null);
@@ -241,12 +241,12 @@ const UserManagementPage: React.FC = () => {
<tr key={user.id} className="border-b border-gray-100 align-top">
<td className="py-3 pr-4">
<div className="font-medium text-gray-900">
{user.user_name}
{user.display_name}
{isSelf ? (
<span className="ml-2 text-xs font-normal text-gray-500">(Anda)</span>
) : null}
</div>
<div className="text-xs text-gray-500">{user.user_login}</div>
<div className="text-xs text-gray-500">{user.user_name}</div>
</td>
<td className="py-3 pr-4">
{!isSelf && editId === user.id ? (
@@ -6,8 +6,8 @@ vi.mock('../../context/AuthContext.tsx', () => ({
useAuth: () => ({
user: {
id: 9,
user_login: 'account_admin',
user_name: 'account_admin',
display_name: 'account_admin',
status: 'account_admin',
is_superuser: false,
created_at: '',
+6 -6
View File
@@ -26,8 +26,8 @@ const {
authState: {
user: {
id: 5,
user_login: 'gm.user',
user_name: 'gm.user',
display_name: 'gm.user',
status: 'gm' as string,
managed_by: null as number | null,
managed_by_name: null as string | null,
@@ -85,8 +85,8 @@ describe('SettingsPage city site registry', () => {
refreshUserMock.mockReset();
authState.user = {
id: 5,
user_login: 'gm.user',
user_name: 'gm.user',
display_name: 'gm.user',
status: 'gm',
managed_by: null,
managed_by_name: null,
@@ -129,13 +129,13 @@ describe('SettingsPage city site registry', () => {
updated_at: '',
});
buhsMock.mockResolvedValue([
{ id: 2, user_login: 'buh', user_name: 'buh', status: 'buh', created_at: '', updated_at: '' },
{ id: 2, user_name: 'buh', display_name: 'buh', status: 'buh', created_at: '', updated_at: '' },
]);
usersListMock.mockResolvedValue([
{
id: 5,
user_login: 'gm',
user_name: 'gm',
display_name: 'gm',
status: 'gm',
managed_by: 2,
created_at: '',
@@ -191,8 +191,8 @@ describe('SettingsPage city site registry', () => {
it('shows pending GM confirmation for BUH', async () => {
authState.user = {
id: 2,
user_login: 'buh',
user_name: 'buh',
display_name: 'buh',
status: 'buh',
managed_by: null,
managed_by_name: null,
@@ -204,8 +204,8 @@ describe('SettingsPage city site registry', () => {
pendingMock.mockResolvedValue([
{
id: 5,
user_login: 'gm.user',
user_name: 'gm.user',
display_name: 'gm.user',
status: 'gm',
created_at: '',
updated_at: '',
@@ -10,8 +10,8 @@ const { listMock, createMock, updateMock, setPasswordMock, authState } = vi.hois
authState: {
user: {
id: 99,
user_login: 'account_admin',
user_name: 'account_admin',
display_name: 'account_admin',
status: 'account_admin' as string,
created_at: '',
updated_at: '',
@@ -45,8 +45,8 @@ describe('UserManagementPage', () => {
setPasswordMock.mockReset();
authState.user = {
id: 99,
user_login: 'account_admin',
user_name: 'account_admin',
display_name: 'account_admin',
status: 'account_admin',
created_at: '',
updated_at: '',
@@ -54,8 +54,8 @@ describe('UserManagementPage', () => {
listMock.mockResolvedValue([
{
id: 1,
user_login: 'gm1.login',
user_name: 'gm1',
user_name: 'gm1.login',
display_name: 'gm1',
status: 'gm',
managed_by_name: 'buh1',
created_at: '',
@@ -63,8 +63,8 @@ describe('UserManagementPage', () => {
},
{
id: 99,
user_login: 'account_admin',
user_name: 'account_admin',
display_name: 'account_admin',
status: 'account_admin',
managed_by_name: null,
created_at: '',
@@ -73,8 +73,8 @@ describe('UserManagementPage', () => {
]);
createMock.mockResolvedValue({
id: 2,
user_login: 'newgm',
user_name: 'newgm',
display_name: 'newgm',
status: 'gm',
created_at: '',
updated_at: '',
@@ -108,8 +108,8 @@ describe('UserManagementPage', () => {
await waitFor(() => {
expect(createMock).toHaveBeenCalledWith({
user_login: 'newgm',
user_name: 'New GM',
user_name: 'newgm',
display_name: 'New GM',
status: 'gm',
password: 'temp123',
});
+3 -3
View File
@@ -30,13 +30,13 @@ const LoginForm: React.FC = () => {
</div>
)}
<div>
<label htmlFor="user_login" className="block text-sm font-medium text-gray-700">
<label htmlFor="user_name" className="block text-sm font-medium text-gray-700">
Login
</label>
<div className="mt-1">
<input
id="user_login"
name="user_login"
id="user_name"
name="user_name"
type="text"
autoComplete="username"
required
+1 -1
View File
@@ -42,7 +42,7 @@ describe('apiClient', () => {
it('sends CSRF token on POST', async () => {
document.cookie = 'csrftoken=abc123; path=/';
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ id: 1, user_name: 'admin' }));
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ id: 1, user_name: 'admin', display_name: 'admin' }));
vi.stubGlobal('fetch', fetchMock);
await api.auth.login('admin', 'admin123');
+2 -2
View File
@@ -159,8 +159,8 @@ const del = (path: string) => request<void>(path, { method: 'DELETE' });
export const api = {
auth: {
csrf: () => get<{ csrfToken: string }>('/auth/csrf/'),
login: (user_login: string, password: string) =>
post<User>('/auth/login/', { user_login, password }),
login: (user_name: string, password: string) =>
post<User>('/auth/login/', { user_name, password }),
logout: () => post<void>('/auth/logout/'),
me: () => get<User>('/auth/me/'),
},
+2 -2
View File
@@ -18,8 +18,8 @@ export type UserStatus =
export type User = {
id: number;
user_login: string;
user_name: string;
display_name: string;
status: UserStatus;
is_superuser?: boolean;
managed_by?: number | null;
@@ -32,8 +32,8 @@ export type User = {
};
export type UserWrite = {
user_login: string;
user_name: string;
display_name: string;
status: UserStatus;
password?: string;
managed_by?: number | null;