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

- 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:
2026-02-20 09:30:58 +03:00
parent dc116270b7
commit b61e1ca8a5
5 changed files with 169 additions and 6 deletions

View 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")

View File

@@ -21,6 +21,7 @@ from duty_teller.db.repository import (
get_duties, get_duties,
insert_duty, insert_duty,
set_user_phone, set_user_phone,
update_user_display_name,
) )
__all__ = [ __all__ = [
@@ -42,6 +43,7 @@ __all__ = [
"get_duties", "get_duties",
"insert_duty", "insert_duty",
"set_user_phone", "set_user_phone",
"update_user_display_name",
"init_db", "init_db",
] ]

View File

@@ -1,6 +1,6 @@
"""SQLAlchemy ORM models for users and duties.""" """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 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) first_name: Mapped[str | None] = mapped_column(Text, nullable=True)
last_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) 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") duties: Mapped[list["Duty"]] = relationship("Duty", back_populates="user")

View File

@@ -23,12 +23,18 @@ def get_or_create_user(
first_name: str | None = None, first_name: str | None = None,
last_name: str | None = None, last_name: str | None = None,
) -> User: ) -> 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) user = get_user_by_telegram_id(session, telegram_user_id)
if user: if user:
user.full_name = full_name
user.username = username user.username = username
user.first_name = first_name if not user.name_manually_edited:
user.last_name = last_name user.full_name = full_name
user.first_name = first_name
user.last_name = last_name
session.commit() session.commit()
session.refresh(user) session.refresh(user)
return user return user
@@ -38,6 +44,7 @@ def get_or_create_user(
username=username, username=username,
first_name=first_name, first_name=first_name,
last_name=last_name, last_name=last_name,
name_manually_edited=False,
) )
session.add(user) session.add(user)
session.commit() 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: 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() user = session.query(User).filter(User.full_name == full_name).first()
if user: if user:
return user return user
@@ -56,6 +66,7 @@ def get_or_create_user_by_full_name(session: Session, full_name: str) -> User:
username=None, username=None,
first_name=None, first_name=None,
last_name=None, last_name=None,
name_manually_edited=True,
) )
session.add(user) session.add(user)
session.commit() session.commit()
@@ -63,6 +74,30 @@ def get_or_create_user_by_full_name(session: Session, full_name: str) -> User:
return 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( def delete_duties_in_range(
session: Session, session: Session,
user_id: int, user_id: int,

View File

@@ -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 import pytest
from sqlalchemy import create_engine 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.models import Base, User
from duty_teller.db.repository import ( from duty_teller.db.repository import (
delete_duties_in_range, delete_duties_in_range,
get_or_create_user,
get_or_create_user_by_full_name, get_or_create_user_by_full_name,
get_duties, get_duties,
insert_duty, 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.id is not None
assert u.full_name == "Новый Пользователь" assert u.full_name == "Новый Пользователь"
assert u.telegram_user_id is None 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): 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 len(rows) == 1
assert rows[0][0].start_at == "2026-01-31T09:00:00Z" assert rows[0][0].start_at == "2026-01-31T09:00:00Z"
assert rows[0][1] == "User A" 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"