Skip to content

Commit 87cf85b

Browse files
committed
fix(user-sync): actually remove pruned fields via empty relations and remove_empty
1 parent 828f02c commit 87cf85b

4 files changed

Lines changed: 34 additions & 14 deletions

File tree

‎docs/tools/user-sync.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ Credentials are read from `accounts.pwd.yaml` (or `--cred-filepath`), the same
2020
mechanism as `OswExpress`. Start with `--dry-run` to preview, then run without
2121
it to write after confirming at the prompt.
2222

23+
To also remove data no longer wanted (disabled optional fields and
24+
`employment_contract_status`) from existing items, add `--prune`:
25+
26+
```bash
27+
uv run python examples/user_sync.py --domain your-instance.example.org --prune --dry-run
28+
```
29+
30+
Users whose ORCID record exposes no email are listed in a warning at the end of
31+
the run; a missing email never fails the sync.
32+
2333
### Options
2434

2535
By default the tool syncs both ORCID and MediaWiki-native accounts and writes

‎src/osw/tools/user_sync/build.py‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,17 @@ def _labels(text: str) -> List[Any]:
2424

2525

2626
def _strip_protected(entity: Any) -> None:
27-
"""Remove protected relation defaults (e.g. employment_contract_status)."""
27+
"""Clear protected relations (e.g. employment_contract_status).
28+
29+
Set to an empty list rather than removed: the model re-applies its default
30+
when the field is absent, and the store only overwrites a relation that is
31+
present as an explicit empty value (with ``remove_empty=False``).
32+
"""
2833
iris = getattr(entity, "__iris__", None)
2934
if isinstance(iris, dict):
3035
for name in PROTECTED_FIELDS:
31-
iris.pop(name, None)
36+
if name in iris:
37+
iris[name] = []
3238

3339

3440
def build_organization(proposed: ProposedOrganization) -> Organization:
@@ -76,10 +82,8 @@ def apply_update(entity: Any, proposed: ProposedUser, apply_fields: Set[str]) ->
7682
if "websites" in apply_fields:
7783
entity.website = set(proposed.websites)
7884
if "organizations" in apply_fields:
79-
if proposed.organizations:
80-
entity.__iris__["organization"] = list(proposed.organizations)
81-
else:
82-
entity.__iris__.pop("organization", None)
85+
# Empty list both links (non-empty) and clears (empty, on removal).
86+
entity.__iris__["organization"] = list(proposed.organizations)
8387
if any(name in apply_fields for name in PROTECTED_FIELDS):
8488
_strip_protected(entity)
8589
return entity

‎src/osw/tools/user_sync/sync.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ def _store_organizations(osw: Any, org_map, report: SyncReport) -> None:
103103
report.failed["organizations"] = str(exc)
104104

105105

106-
def _store_users(osw: Any, resolution: Resolution, report: SyncReport) -> None:
106+
def _store_users(
107+
osw: Any, resolution: Resolution, report: SyncReport, remove_empty: bool = True
108+
) -> None:
107109
entities = []
108110
for resolved in resolution.resolved:
109111
proposed = resolved.change.proposed
@@ -126,7 +128,10 @@ def _store_users(osw: Any, resolution: Resolution, report: SyncReport) -> None:
126128
try:
127129
osw.store_entity(
128130
OSW.StoreEntityParam(
129-
entities=entities, overwrite=True, edit_comment="user-sync"
131+
entities=entities,
132+
overwrite=True,
133+
remove_empty=remove_empty,
134+
edit_comment="user-sync",
130135
)
131136
)
132137
except Exception as exc: # pragma: no cover - network failure path
@@ -215,7 +220,8 @@ def run_user_sync(
215220

216221
if config.link_organizations:
217222
_store_organizations(osw, org_map, report)
218-
_store_users(osw, resolution, report)
223+
# When pruning, keep emptied fields in the payload so they overwrite (delete).
224+
_store_users(osw, resolution, report, remove_empty=not config.prune)
219225
if config.create_redirects:
220226
_create_redirects(osw, resolution, report)
221227
_verify(osw, report)

‎tests/tools/test_user_sync_build.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,17 @@ def _entity():
3838
)
3939

4040

41-
def test_strip_protected_removes_only_protected():
41+
def test_strip_protected_empties_only_protected():
4242
entity = _entity()
4343
_strip_protected(entity)
44-
assert "employment_contract_status" not in entity.__iris__
45-
assert "organization" in entity.__iris__
44+
assert entity.__iris__["employment_contract_status"] == []
45+
assert entity.__iris__["organization"] == ["Item:OSWa"]
4646

4747

4848
def test_apply_update_strips_protected_when_in_fields():
4949
entity = _entity()
5050
apply_update(entity, _proposed(), {"employment_contract_status"})
51-
assert "employment_contract_status" not in entity.__iris__
51+
assert entity.__iris__["employment_contract_status"] == []
5252

5353

5454
def test_apply_update_keeps_protected_when_not_in_fields():
@@ -61,7 +61,7 @@ def test_apply_update_keeps_protected_when_not_in_fields():
6161
def test_apply_update_clears_organization_when_empty():
6262
entity = _entity()
6363
apply_update(entity, _proposed(organizations=[]), {"organizations"})
64-
assert "organization" not in entity.__iris__
64+
assert entity.__iris__["organization"] == []
6565

6666

6767
def test_apply_update_sets_organization_when_present():

0 commit comments

Comments
 (0)