Repository: dag-hammarskjold-library/dlx

Issue:
Update bib record when tag in linked auth changes
Adresses https://github.com/dag-hammarskjold-library/dlx-rest/issues/1262

Discussion:

Solution patch:
diff --git a/dlx/marc/__init__.py b/dlx/marc/__init__.py
index dd8bde5e..1a1d6382 100644
--- a/dlx/marc/__init__.py
+++ b/dlx/marc/__init__.py
@@ -854,6 +854,7 @@ def index_field_text(*, threaded=True):
                 return ' '.join(all_text)
             except Exception as err:
                 LOGGER.exception(err)
+                raise err
 
         # all-text
         self.text = index_field_text(threaded=False)
@@ -937,6 +938,7 @@ def index_logical_fields():
                     DB.handle[f'_index_{logical_field}'].bulk_write(updates)
             except Exception as err:
                 LOGGER.exception(err)
+                raise err
 
         # assign logical fields here
         for field, vals in self.logical_fields().items(): 
@@ -987,6 +989,7 @@ def save_history():
                 history_collection.replace_one({'_id': self.id}, record_history, upsert=True)
             except Exception as err:
                 LOGGER.exception(err)
+                raise err
 
         if DB.database_name == 'testing':
             save_history()
@@ -1001,6 +1004,7 @@ def save_history():
         if not result.acknowledged:
             raise Exception('Commit failed')
         
+        # manage caches
         if isinstance(self, Auth):
             # clear these caches
             for cache in ('_xcache', '_pcache', '_langcache'):
@@ -1016,33 +1020,44 @@ def save_history():
         # auth attached records update
         def update_attached_records(auth):
             try:
-                    if auth.record_type != 'auth': raise Exception('Record type must be auth')
-
-                    for record in auth.list_attached():
-                        def do_update():
-                            try:
-                                if isinstance(record, Auth):
-                                    # prevent feedback loops
-                                    if auth.id in [x.id for x in record.list_attached(usage_type='auth')]:
-                                        record.commit(user=auth.user, auth_check=False, update_attached=False)
-                                        return
-
-                                record.commit(user=auth.user, auth_check=False)
-                            except Exception as err:
-                                LOGGER.exception(err)
-
-                            # for debugging
-                            DB.handle['auth_linked_update_log'].insert_one({'record_type': record.record_type, 'record_id': record.id, 'action': 'updated', 'triggered_by': auth.id, 'time': datetime.now()})
-
-                        if 1: #DB.database_name == 'testing':
-                            do_update()
-                        else:
-                            # subthreading here doesn't work ?
-                            subthread = threading.Thread(target=do_update, args=[])
-                            subthread.setDaemon(False) # stop the thread after complete
-                            subthread.start()
+                if auth.record_type != 'auth': raise Exception('Record type must be auth')
+
+                for record in auth.list_attached():
+                    def do_update():
+                        try:
+                            if isinstance(record, Auth):
+                                # prevent feedback loops
+                                if auth.id in [x.id for x in record.list_attached(usage_type='auth')]:
+                                    record.commit(user=auth.user, auth_check=False, update_attached=False)
+                                    return
+
+                            # if the heading field tag changed, change the tag in linked record
+                            if old_tag := Auth(previous_state).heading_field.tag:
+                                if old_tag != self.heading_field.tag:
+                                    for field in record.datafields:
+                                        for subfield in filter(lambda x: hasattr(x, 'xref'), field.subfields):
+                                            if subfield.xref == self.id:
+                                                new_tag = field.tag[0] + self.heading_field.tag[1:]
+                                                field.tag = new_tag
+
+                            record.commit(user=auth.user, auth_check=False)
+                        except Exception as err:
+                            LOGGER.exception(err)
+                            raise err
+
+                        # for debugging
+                        DB.handle['auth_linked_update_log'].insert_one({'record_type': record.record_type, 'record_id': record.id, 'action': 'updated', 'triggered_by': auth.id, 'time': datetime.now()})
+
+                    if 1: #DB.database_name == 'testing':
+                        do_update()
+                    else:
+                        # subthreading here doesn't work ?
+                        subthread = threading.Thread(target=do_update, args=[])
+                        subthread.setDaemon(False) # stop the thread after complete
+                        subthread.start()
             except Exception as err:
                     LOGGER.exception(err)
+                    raise err
 
         if isinstance(self, Auth) and update_attached == True:
             if previous_state:
@@ -1053,7 +1068,7 @@ def do_update():
                     heading_serialized = [(x.code, x.value) for x in list(filter(lambda x: x.code in linked_codes, self.heading_field.subfields))]
                     prev_serialized = [(x.code, x.value) for x in list(filter(lambda x: x.code in linked_codes, previous.heading_field.subfields))]
 
-                    if heading_serialized != prev_serialized:
+                    if heading_serialized != prev_serialized or self.heading_field.tag != previous.heading_field.tag:
                         # the heading has changed
                         if DB.database_name == 'testing': 
                             update_attached_records(self)
@@ -1093,6 +1108,7 @@ def update_browse_collections():
                         DB.handle[f'_index_{field}'].bulk_write(updates)
             except Exception as err:
                 LOGGER.exception(err)
+                raise err
 
         if DB.database_name == 'testing': 
             update_browse_collections()
@@ -1765,9 +1781,6 @@ def heading_field(self):
         -------
         dlx.marc.Datafield
         """
-        
-        if self._heading_field:
-            return self._heading_field
             
         self._heading_field = next(filter(lambda field: field.tag[0:1] == '1', self.fields), None)
         
@@ -1842,7 +1855,7 @@ def count(lookup_class, xref):
     def list_attached(self, usage_type=None):
         """List the records attached to this auth record"""
 
-        def list_records(lookup_class, xref):
+        def list_records(lookup_class, xref: int) -> list[Marc]:
             tags = list(Config.bib_authority_controlled.keys()) if lookup_class == BibSet else list(Config.auth_authority_controlled.keys())
             return [record for tag in tags for record in lookup_class.from_query({f'{tag}.subfields.xref': xref})]
 
@@ -1906,6 +1919,7 @@ def do_commit():
                             record.commit(user=user, auth_check=False)
                         except Exception as err:
                             LOGGER.exception(err)
+                            raise err
 
                         # for debugging
                         DB.handle['merge_log'].insert_one({'record_type': record_type, 'record_id': record.id, 'action': 'updated', 'time': datetime.now()})


Test patch:
diff --git a/tests/test_marc.py b/tests/test_marc.py
index a43e3672..d0914c07 100644
--- a/tests/test_marc.py
+++ b/tests/test_marc.py
@@ -83,46 +83,46 @@ def test_commit(db, bibs, auths):
     from bson import ObjectId
     from jsonschema.exceptions import ValidationError
 
-    for bib in [Bib(x) for x in bibs]:
-        assert bib.commit() == bib
+    for linked_bib in [Bib(x) for x in bibs]:
+        assert linked_bib.commit() == linked_bib
         
     for auth in [Auth(x) for x in auths]:
         assert auth.commit() == auth
         
-    bib = Bib()
-    assert bib.commit() == bib
-    assert isinstance(bib.updated, datetime)
-    assert bib.user == 'admin'
-    assert bib.history()[0].to_dict() == bib.to_dict()
-    assert bib.history()[0].user == 'admin'
+    linked_bib = Bib()
+    assert linked_bib.commit() == linked_bib
+    assert isinstance(linked_bib.updated, datetime)
+    assert linked_bib.user == 'admin'
+    assert linked_bib.history()[0].to_dict() == linked_bib.to_dict()
+    assert linked_bib.history()[0].user == 'admin'
     # there are two bibs before this in the db from conftest
     assert Bib.max_id() == 3
 
     # new audit attributes
-    assert bib.created == bib.updated
-    assert bib.created_user == 'admin'
-    bib.commit(user='different user')
-    assert bib.created != bib.updated
-    bib.created = 'string'
-    bib.commit()
-    assert isinstance(bib.created, datetime) # can't change created
-    assert bib.created_user == 'admin'
+    assert linked_bib.created == linked_bib.updated
+    assert linked_bib.created_user == 'admin'
+    linked_bib.commit(user='different user')
+    assert linked_bib.created != linked_bib.updated
+    linked_bib.created = 'string'
+    linked_bib.commit()
+    assert isinstance(linked_bib.created, datetime) # can't change created
+    assert linked_bib.created_user == 'admin'
 
     # new text attributes
-    bib.set('245', 'a', 'TESTING TESTING 1234')
-    bib.commit()
-    assert bib.text == 'testing testing 1234'
-    assert bib.words == ['test', 'test', '1234']
+    linked_bib.set('245', 'a', 'TESTING TESTING 1234')
+    linked_bib.commit()
+    assert linked_bib.text == 'testing testing 1234'
+    assert linked_bib.words == ['test', 'test', '1234']
 
     # id incrementer
     Bib().commit()
-    bib = Bib().commit()
-    assert bib.id == Bib.max_id() == 5
+    linked_bib = Bib().commit()
+    assert linked_bib.id == Bib.max_id() == 5
     
     # don't reuse id
-    bib.delete()
-    bib = Bib().commit()
-    assert bib.id == 6
+    linked_bib.delete()
+    linked_bib = Bib().commit()
+    assert linked_bib.id == 6
     
     # max id resets
     DB.bibs.drop()
@@ -133,26 +133,36 @@ def test_commit(db, bibs, auths):
     
     # json schema validation
     with pytest.raises(ValidationError):
-        bib = Bib({'245': [{'indicators': [' ', ' '], 'subfields': [{'code': ' ', 'value': 'Subfield code is a space'}]}]})
-        bib.commit()
+        linked_bib = Bib({'245': [{'indicators': [' ', ' '], 'subfields': [{'code': ' ', 'value': 'Subfield code is a space'}]}]})
+        linked_bib.commit()
 
     # update attached records
-    auth = Auth()
+    auth = Auth().set('150', 'a', 'to be updated').commit()
     bibs = [Bib().set('650', 'a', auth.id).commit() for i in range(10)]
-    auths = [Auth().set('550', 'a', auth.id).commit() for i in range(10)]
+    auths = [Auth().set('150', 'a', 'related').set('550', 'a', auth.id).commit() for i in range(10)]
     auth.set('150', 'a', 'updated')
     auth.commit(user='test auth update')
+
+    assert len(auth.list_attached()) == 20
     
-    for bib in auth.list_attached():
-        assert bib.get_value('650', 'a') == 'updated'
-        assert bib.user == 'test auth update'
+    for linked_bib in auth.list_attached(usage_type='bib'):
+        assert linked_bib.get_value('650', 'a') == 'updated'
+        assert linked_bib.user == 'test auth update'
 
         # log
-        assert DB.handle['auth_linked_update_log'].find_one({'record_type': 'bib', 'record_id': bib.id})['triggered_by'] == auth.id
+        assert DB.handle['auth_linked_update_log'].find_one({'record_type': 'bib', 'record_id': linked_bib.id})['triggered_by'] == auth.id
+
+    for linked_auth in auth.list_attached(usage_type='auth'):
+        assert linked_auth.get_value('550', 'a') == 'updated'
+        assert linked_auth.user == 'test auth update'
+
+    # tag change
+    auth.heading_field.tag = '100'
+    auth.commit()
 
-    for auth in auth.list_attached():
-        assert auth.get_value('550', 'a') == 'updated'
-        assert auth.user == 'test auth update'
+    for linked_bib in auth.list_attached(usage_type='bib'):
+        assert linked_bib.get_field('600')
+        assert linked_bib.get_field('650') is None
 
 def test_delete(db):
     from dlx import DB