feat: add name_manually_edited field to User model and update related functionality
All checks were successful
CI / lint-and-test (push) Successful in 15s
All checks were successful
CI / lint-and-test (push) Successful in 15s
- Introduced a new boolean column `name_manually_edited` in the `users` table to control whether user names are overwritten during synchronization with Telegram. - Updated the `get_or_create_user` function to respect the `name_manually_edited` flag, ensuring names are only updated when the flag is false. - Implemented a new function `update_user_display_name` to allow manual updates of user names while setting the `name_manually_edited` flag to true. - Enhanced unit tests to cover the new functionality and ensure correct behavior of name handling based on the `name_manually_edited` flag.
This commit is contained in:
35
alembic/versions/006_name_manually_edited.py
Normal file
35
alembic/versions/006_name_manually_edited.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Add name_manually_edited to users
|
||||
|
||||
Revision ID: 006
|
||||
Revises: 005
|
||||
Create Date: 2025-02-19
|
||||
|
||||
When True, full_name/first_name/last_name are not overwritten by get_or_create_user
|
||||
(Telegram sync). Set manually or via update_user_display_name when editing name in DB/API.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "006"
|
||||
down_revision: Union[str, None] = "005"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column(
|
||||
"name_manually_edited",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text("0"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("users", "name_manually_edited")
|
||||
@@ -21,6 +21,7 @@ from duty_teller.db.repository import (
|
||||
get_duties,
|
||||
insert_duty,
|
||||
set_user_phone,
|
||||
update_user_display_name,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -42,6 +43,7 @@ __all__ = [
|
||||
"get_duties",
|
||||
"insert_duty",
|
||||
"set_user_phone",
|
||||
"update_user_display_name",
|
||||
"init_db",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""SQLAlchemy ORM models for users and duties."""
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, BigInteger, Text
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, BigInteger, Text
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ class User(Base):
|
||||
first_name: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
last_name: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
phone: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
name_manually_edited: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, server_default="0", default=False
|
||||
)
|
||||
|
||||
duties: Mapped[list["Duty"]] = relationship("Duty", back_populates="user")
|
||||
|
||||
|
||||
@@ -23,12 +23,18 @@ def get_or_create_user(
|
||||
first_name: str | None = None,
|
||||
last_name: str | None = None,
|
||||
) -> User:
|
||||
"""
|
||||
Get or create user by telegram_user_id. On create, name comes from Telegram.
|
||||
On update: username is always synced; full_name/first_name/last_name are only
|
||||
updated if name_manually_edited is False (otherwise keep existing display name).
|
||||
"""
|
||||
user = get_user_by_telegram_id(session, telegram_user_id)
|
||||
if user:
|
||||
user.full_name = full_name
|
||||
user.username = username
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
if not user.name_manually_edited:
|
||||
user.full_name = full_name
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
@@ -38,6 +44,7 @@ def get_or_create_user(
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
name_manually_edited=False,
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
@@ -46,7 +53,10 @@ def get_or_create_user(
|
||||
|
||||
|
||||
def get_or_create_user_by_full_name(session: Session, full_name: str) -> User:
|
||||
"""Find user by exact full_name or create one with telegram_user_id=None (for duty-schedule import)."""
|
||||
"""
|
||||
Find user by exact full_name or create one with telegram_user_id=None (for duty-schedule import).
|
||||
New users get name_manually_edited=True since the name comes from import, not Telegram.
|
||||
"""
|
||||
user = session.query(User).filter(User.full_name == full_name).first()
|
||||
if user:
|
||||
return user
|
||||
@@ -56,6 +66,7 @@ def get_or_create_user_by_full_name(session: Session, full_name: str) -> User:
|
||||
username=None,
|
||||
first_name=None,
|
||||
last_name=None,
|
||||
name_manually_edited=True,
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
@@ -63,6 +74,30 @@ def get_or_create_user_by_full_name(session: Session, full_name: str) -> User:
|
||||
return user
|
||||
|
||||
|
||||
def update_user_display_name(
|
||||
session: Session,
|
||||
telegram_user_id: int,
|
||||
full_name: str,
|
||||
first_name: str | None = None,
|
||||
last_name: str | None = None,
|
||||
) -> User | None:
|
||||
"""
|
||||
Update display name for user by telegram_user_id and set name_manually_edited=True.
|
||||
Use from API or admin when name is changed manually; subsequent get_or_create_user
|
||||
will not overwrite these fields. Returns User or None if not found.
|
||||
"""
|
||||
user = session.query(User).filter(User.telegram_user_id == telegram_user_id).first()
|
||||
if not user:
|
||||
return None
|
||||
user.full_name = full_name
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.name_manually_edited = True
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
def delete_duties_in_range(
|
||||
session: Session,
|
||||
user_id: int,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for delete_duties_in_range and get_or_create_user_by_full_name."""
|
||||
"""Tests for delete_duties_in_range, get_or_create_user_by_full_name, name_manually_edited."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
@@ -7,9 +7,11 @@ from sqlalchemy.orm import sessionmaker
|
||||
from duty_teller.db.models import Base, User
|
||||
from duty_teller.db.repository import (
|
||||
delete_duties_in_range,
|
||||
get_or_create_user,
|
||||
get_or_create_user_by_full_name,
|
||||
get_duties,
|
||||
insert_duty,
|
||||
update_user_display_name,
|
||||
)
|
||||
|
||||
|
||||
@@ -47,6 +49,7 @@ def test_get_or_create_user_by_full_name_creates(session):
|
||||
assert u.id is not None
|
||||
assert u.full_name == "Новый Пользователь"
|
||||
assert u.telegram_user_id is None
|
||||
assert u.name_manually_edited is True
|
||||
|
||||
|
||||
def test_get_or_create_user_by_full_name_returns_existing(session, user_a):
|
||||
@@ -107,3 +110,88 @@ def test_get_duties_includes_duty_starting_on_last_day_of_range(session, user_a)
|
||||
assert len(rows) == 1
|
||||
assert rows[0][0].start_at == "2026-01-31T09:00:00Z"
|
||||
assert rows[0][1] == "User A"
|
||||
|
||||
|
||||
def test_get_or_create_user_overwrites_name_when_flag_false(session):
|
||||
"""When name_manually_edited is False, second get_or_create_user overwrites name."""
|
||||
u1 = get_or_create_user(
|
||||
session,
|
||||
telegram_user_id=100,
|
||||
full_name="First Name",
|
||||
username="user1",
|
||||
first_name="First",
|
||||
last_name="Name",
|
||||
)
|
||||
assert u1.full_name == "First Name"
|
||||
assert u1.name_manually_edited is False
|
||||
u2 = get_or_create_user(
|
||||
session,
|
||||
telegram_user_id=100,
|
||||
full_name="Second Name",
|
||||
username="user2",
|
||||
first_name="Second",
|
||||
last_name="Name",
|
||||
)
|
||||
assert u2.id == u1.id
|
||||
assert u2.full_name == "Second Name"
|
||||
assert u2.first_name == "Second"
|
||||
assert u2.last_name == "Name"
|
||||
assert u2.username == "user2"
|
||||
|
||||
|
||||
def test_get_or_create_user_keeps_name_when_flag_true_updates_username(session):
|
||||
"""When name_manually_edited is True, get_or_create_user keeps name but updates username."""
|
||||
u1 = get_or_create_user(
|
||||
session,
|
||||
telegram_user_id=200,
|
||||
full_name="Custom Name",
|
||||
username="old_username",
|
||||
first_name="Custom",
|
||||
last_name="Name",
|
||||
)
|
||||
u1.name_manually_edited = True
|
||||
session.commit()
|
||||
session.refresh(u1)
|
||||
u2 = get_or_create_user(
|
||||
session,
|
||||
telegram_user_id=200,
|
||||
full_name="Telegram Name",
|
||||
username="new_username",
|
||||
first_name="Telegram",
|
||||
last_name="Name",
|
||||
)
|
||||
assert u2.id == u1.id
|
||||
assert u2.full_name == "Custom Name"
|
||||
assert u2.first_name == "Custom"
|
||||
assert u2.last_name == "Name"
|
||||
assert u2.username == "new_username"
|
||||
|
||||
|
||||
def test_update_user_display_name_sets_flag_then_get_or_create_user_keeps_name(session):
|
||||
"""update_user_display_name sets name and flag; get_or_create_user then does not overwrite name."""
|
||||
get_or_create_user(
|
||||
session,
|
||||
telegram_user_id=300,
|
||||
full_name="Original",
|
||||
username="u3",
|
||||
first_name="Original",
|
||||
last_name=None,
|
||||
)
|
||||
updated = update_user_display_name(
|
||||
session, 300, "Manual Name", first_name="Manual", last_name="Name"
|
||||
)
|
||||
assert updated is not None
|
||||
assert updated.full_name == "Manual Name"
|
||||
assert updated.name_manually_edited is True
|
||||
u_after_sync = get_or_create_user(
|
||||
session,
|
||||
telegram_user_id=300,
|
||||
full_name="From Telegram",
|
||||
username="new_u3",
|
||||
first_name="From",
|
||||
last_name="Telegram",
|
||||
)
|
||||
assert u_after_sync.full_name == "Manual Name"
|
||||
assert u_after_sync.first_name == "Manual"
|
||||
assert u_after_sync.last_name == "Name"
|
||||
assert u_after_sync.username == "new_u3"
|
||||
|
||||
Reference in New Issue
Block a user