diff --git a/ChangeLog.md b/ChangeLog.md index 8678a34..9bb7ac1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,15 @@ All notable changes to Hermes will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- [Clients] Fixed Jinja expressions relying on multiple attributes not rendered properly when one of them was removed (#1). + **IMPORTANT NOTE** This bug only affects clients that contain Jinja expressions that rely on multiple attributes from the server. If your clients are likely to be impacted, here are the steps to fix invalid values (proceed on each client): + 1. Edit the client's config file, and in each data-type declared and maybe impacted, insert a non-significant space into the Jinja expression : e.g. replace `{{ attr1 ~ attr2 }}` by `{{ attr1 ~ attr2 }}`. This simple change will trigger a datamodel update on the next client startup, which will recalculate the values of all Jinja expressions and propagate the new (good) values if they differ from the previous ones. + 2. Restart your client. + ## [v1.0.5] - 2025-07-08 ### Fixed diff --git a/clients/datamodel.py b/clients/datamodel.py index c7f7845..1d383c7 100644 --- a/clients/datamodel.py +++ b/clients/datamodel.py @@ -509,13 +509,13 @@ def convertEventToLocal( # Handle that event.objattrs is 1 depth deeper for "modified" events if event.eventtype == "modified": sources = ("added", "modified", "removed") + objattrs = {"added": {}, "modified": {}, "removed": {}} else: sources = (None,) + objattrs = {} hasContent: bool = False - objattrs = {} for source in sources: - attrs = {} if source is None: src = event.objattrs else: @@ -549,17 +549,43 @@ def convertEventToLocal( if type(val) is list: val = [v for v in val if v is not None] - if source == "removed" or (val is not None and val != []): - attrs[dest] = val - else: - attrs[dest] = v - if attrs: - hasContent = True + if val is None or val == []: + # No value + if event.eventtype == "modified": + objattrs["removed"].update({dest: val}) + elif event.eventtype == "removed": + objattrs.update({dest: val}) + else: + # In modified events, we have to determine if the + # attribute is added or modified + if event.eventtype == "modified": + _, cachedObj = self.getObjectFromCacheOrTrashbin( + self.localdata_complete, + self.typesmapping[event.objtype], + event.objpkey, + ) - if source is None: - objattrs = attrs - else: - objattrs[source] = attrs + if cachedObj is not None and hasattr( + cachedObj, dest + ): + # Ensure the value has changed + previousVal = getattr(cachedObj, dest) + if DataObject.isDifferent(previousVal, val): + objattrs["modified"].update({dest: val}) + hasContent = True + else: + # Attr is added + objattrs["added"].update({dest: val}) + hasContent = True + else: + objattrs.update({dest: val}) + hasContent = True + else: + if source is None: + objattrs.update({dest: v}) + else: + objattrs[source].update({dest: v}) + hasContent = True res = None if hasContent or allowEmptyEvent or event.eventtype == "removed": diff --git a/tests/functional/fixtures/config_files/client.yml b/tests/functional/fixtures/config_files/client.yml index 81910bb..7a86290 100644 --- a/tests/functional/fixtures/config_files/client.yml +++ b/tests/functional/fixtures/config_files/client.yml @@ -86,6 +86,7 @@ hermes-client: first_name: first_name middle_name: middle_name last_name: last_name + displayname: "{{ (first_name ~ ' ' ~ last_name ~ ' (Engineering)') if 'engineering' in (specialty|default('')) else first_name ~ ' ' ~ last_name }}" dateOfBirth: dateOfBirth login: login specialty: specialty @@ -113,6 +114,7 @@ hermes-client: first_name: first_name middle_name: middle_name last_name: last_name + displayname: "{{ (first_name ~ ' ' ~ last_name ~ ' (Engineering)') if 'engineering' in (specialty|default('')) else first_name ~ ' ' ~ last_name }}" dateOfBirth: dateOfBirth login: login specialty: specialty @@ -142,6 +144,7 @@ hermes-client: first_name: first_name middle_name: middle_name last_name: last_name + displayname: "{{ (first_name ~ ' ' ~ last_name ~ ' (Engineering)') if 'engineering' in (specialty|default('')) else first_name ~ ' ' ~ last_name }}" dateOfBirth: dateOfBirth login: login specialty: specialty diff --git a/tests/functional/test_scenario_01_single_datasource.py b/tests/functional/test_scenario_01_single_datasource.py index 65b2340..8312ea5 100644 --- a/tests/functional/test_scenario_01_single_datasource.py +++ b/tests/functional/test_scenario_01_single_datasource.py @@ -257,12 +257,14 @@ def test_005_update_values(self): "dateOfBirth": "1965-01-13T12:34:56", # Modify time "desired_jobs_joined": "Arboriculturist", # Remove Copywriter, advertising "desired_job_1": None, # Remove Copywriter, advertising + "specialty": None, # Remove Automotive engineering } expectedjvang = deepcopy(self.serverdata("SRVUsers")[jvanguid].toNative()) expectedjvang["middle_name"] = "Jack" expectedjvang["dateOfBirth"] = datetime(1965, 1, 13, 12, 34, 56) expectedjvang["desired_jobs_joined"].remove("Copywriter, advertising") expectedjvang["desired_jobs_columns"].remove("Copywriter, advertising") + del expectedjvang["specialty"] mpateluid = "97fe56c5-4c9a-4f24-97b4-c294bd44089d" mpatel = { @@ -307,12 +309,14 @@ def test_005_update_values(self): self.assertClientdataLen() expectedjvang["_pkey_id"] = jvanguid + expectedjvang["displayname"] = "Joe Vang" del expectedjvang["id"] del expectedjvang["simpleid"] self.assertDictEqual( expectedjvang, self.clientdata("Users")[jvanguid].toNative() ) expectedmpatel["_pkey_id"] = mpateluid + expectedmpatel["displayname"] = "Maria Patel" del expectedmpatel["id"] del expectedmpatel["simpleid"] self.assertDictEqual( @@ -1130,6 +1134,7 @@ def test_401b_client_restore_trashed_user(self): "_pkey_id": tmays_id, "first_name": "Troy", "last_name": "Mays", + "displayname": "Troy Mays", "dateOfBirth": datetime(1977, 12, 9, 0, 0), "login": "tmays", "specialty": "Mechatronics", @@ -1162,6 +1167,7 @@ def test_401b_client_restore_trashed_user(self): "_pkey_id": tmays_id, "first_name": "Troy", "last_name": "Mays", + "displayname": "Troy Mays", "login": "tmays_modified", "specialty": "Mechatronics", "desired_jobs_joined": [ @@ -1294,6 +1300,7 @@ def test_501e_client_maxremediation_removed_then_added_with_previous_added( # Verify that cached data is expected data expectedtwagner = deepcopy(self.serverdata("SRVUsers")[twagneruid].toNative()) expectedtwagner["_pkey_id"] = twagneruid + expectedtwagner["displayname"] = "Tamara Wagner (Engineering)" del expectedtwagner["id"] del expectedtwagner["simpleid"] self.assertDictEqual( @@ -1398,6 +1405,7 @@ def test_502f_client_maxremediation_removed_then_added_with_prev_local_modified( # Verify that cached data is expected data expectedtwagner = deepcopy(self.serverdata("SRVUsers")[twagneruid].toNative()) expectedtwagner["_pkey_id"] = twagneruid + expectedtwagner["displayname"] = "Tamara Wagner (Engineering)" del expectedtwagner["id"] del expectedtwagner["simpleid"] self.assertDictEqual(