Repository: dag-hammarskjold-library/dlx

Issue:
Bump version to v1.4.20

Discussion:

Solution patch:
diff --git a/dlx/marc/__init__.py b/dlx/marc/__init__.py
index ace6197c..d90cf380 100644
--- a/dlx/marc/__init__.py
+++ b/dlx/marc/__init__.py
@@ -594,7 +594,7 @@ def get_field(self, tag, place=0):
     def get_values(self, tag, *codes, **kwargs):
         if tag[:2] == '00':
             return [field.value for field in self.get_fields(tag)]
-            
+           
         values = [sub.value for sub in self.get_subfields(tag, *codes, place=kwargs.get('place'))]
 
         return list(filter(None, values))
@@ -661,6 +661,9 @@ def get_subfields(self, tag, *codes, **kwargs):
         for field in fields:
             if isinstance(field, Controlfield):
                 return
+            
+            if not codes:
+                codes = list(set([x.code for x in field.subfields]))
 
             subs += list(filter(lambda x: x.code in codes, field.subfields))
 
@@ -769,7 +772,7 @@ def validate(self):
         except jsonschema.exceptions.ValidationError as e:
             msg = '{} in {} : {}'.format(e.message, str(list(e.path)), self.to_json())
             raise jsonschema.exceptions.ValidationError(msg)
-
+    
     @Decorators.check_connected
     def commit(self, user='admin', auth_check=True, update_attached=True):
         new_record = True if self.id is None else False
@@ -840,7 +843,7 @@ def index_field_text(*, threaded=True):
                                 {'_id': text},
                                 {'$addToSet': {'subfields': {'code': subfield.code, 'value': subfield.value}}},
                                 upsert=True
-                            ) for subfield in field.subfields
+                            ) for subfield in field.subfields if subfield.value
                         ]
 
                         words = Tokenizer.tokenize(text)
@@ -1038,20 +1041,28 @@ def update_attached_records(auth):
                 for record in auth.list_attached():
                     def do_update():
                         try:
-                            if isinstance(record, Auth):
+                            if isinstance(record, Auth) and auth.id in [x.id for x in record.list_attached(usage_type='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, 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
+                            if self.heading_field.tag != Auth(previous_state).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
+
+                            # if any subfields have been deleted, delete them from the linked record
+                            codes_removed = []
+
+                            for subfield in Auth(previous_state).heading_field.subfields:   
+                                if subfield.code not in [x.code for x in self.heading_field.subfields]:
+                                    codes_removed.append(subfield.code)
+
+                            for linked_field in [x for x in record.fields if isinstance(x, Datafield)]:                                          
+                                linked_field.subfields = [x for x in linked_field.subfields if x.code not in codes_removed]
 
                             record.commit(user=auth.user, auth_check=False)
                         except Exception as err:
@@ -2186,6 +2197,9 @@ def get_value(self, code, *, place=0):
         return sub.value if sub else ''
 
     def get_values(self, *codes):
+        if not codes:
+            codes = list(set([x.code for x in self.subfields]))
+
         values = []
 
         for code in codes:
@@ -2214,6 +2228,11 @@ def get_subfield(self, code, place=None):
             if i == place:
                 return sub
 
+    def delete_subfield(self, code: str):
+        self.subfields = [x for x in self.subfields if x != code]
+
+        return self
+
     def set(self, code, new_val, *, ind1=None, ind2=None, place=0, auth_control=True):
         if not new_val and not ind1 and not ind2:
             return self
diff --git a/dlx/marc/query.py b/dlx/marc/query.py
index fc0ab175..47abc1b2 100644
--- a/dlx/marc/query.py
+++ b/dlx/marc/query.py
@@ -181,6 +181,8 @@ def parse(token, modifier=None):
                     for m in matches:
                         matched_subfield_values += [x['value'] for x in filter(lambda z: z['code'] == code, m['subfields'])]
 
+                    matched_subfield_values = list(filter(None, matched_subfield_values))
+
                     stemmed_terms, filtered = Tokenizer.tokenize(value), []
 
                     for val in matched_subfield_values:
@@ -302,7 +304,7 @@ def parse(token, modifier=None):
                     for m in matches:
                         matched_subfield_values += [x['value'] for x in m['subfields']]
 
-                    matched_subfield_values = list(set(matched_subfield_values))
+                    matched_subfield_values = list(set(filter(None, matched_subfield_values)))
                     stemmed_terms, filtered = Tokenizer.tokenize(value), []
 
                     for val in matched_subfield_values:
diff --git a/dlx/scripts/build_text_collections.py b/dlx/scripts/build_text_collections.py
index 038fec18..04d1e716 100644
--- a/dlx/scripts/build_text_collections.py
+++ b/dlx/scripts/build_text_collections.py
@@ -67,7 +67,7 @@ def run():
                 # add to seen index
                 seen.setdefault(field.tag, {})
                 seen[field.tag].setdefault(text, {})
-                seen[field.tag][text]['subfields'] = [{'code': x.code, 'value': x.value} for x in field.subfields]
+                seen[field.tag][text]['subfields'] = [{'code': x.code, 'value': x.value} for x in field.subfields if x.value]
 
                 updates.setdefault(field.tag, [])
                 


Test patch:
diff --git a/tests/test_marc.py b/tests/test_marc.py
index ef93d5bd..5bebcfab 100644
--- a/tests/test_marc.py
+++ b/tests/test_marc.py
@@ -164,6 +164,17 @@ def test_commit(db, bibs, auths):
         assert linked_bib.get_field('600')
         assert linked_bib.get_field('650') is None
 
+    # subfield deleted
+    auth = Auth()
+    auth.set('100', 'a', 'will not be deleted').set('100', 'g', 'subfield to be deleted').commit()
+    bib = Bib()
+    bib.set('600', 'a', auth.id).set('600', 'g', auth.id).commit()
+    auth.heading_field.subfields = [x for x in auth.heading_field.subfields if x.code != 'g']
+    auth.commit()
+    bib = Bib.from_id(bib.id) # re-retrive the updated data from the db that was updated in the backgroud
+    assert len([x for x in auth.heading_field.subfields]) == 1
+    assert len([x for x in bib.get_field('600').subfields]) == 1
+
 def test_delete(db):
     from dlx import DB
     from dlx.marc import Bib
@@ -502,7 +513,16 @@ def test_get_value(db, bibs):
     assert bib.get_values('520', 'a', place=1) == ['Another description', 'Repeated subfield']
     assert bib.get_value('999', 'a') == ''
     assert bib.get_values('999', 'a') == []
-    
+
+def test_get_values(db, bibs):
+    from dlx.marc import Bib
+
+    bib = Bib(bibs[0])
+    assert bib.get_values('520') == ['Description', 'Another description', 'Repeated subfield']
+
+    # field level
+    assert bib.get_field('520', place=1).get_values() == ['Another description', 'Repeated subfield']
+
 def test_get_xref(db, bibs):
     from dlx.marc import Bib
     
@@ -985,4 +1005,17 @@ def test_history(db):
             datetime.fromisoformat('2999-12-31') # set in the future because the delete is too fast here
         ), 
         None
-    ) == bib.id
\ No newline at end of file
+    ) == bib.id
+
+def test_auth_deleted_subfield(db):
+    from dlx.marc import Bib, Auth, Query
+
+    # sequence of events formerly causing an error
+    auth = Auth().set('110', 'a', 'will NOT be deleted').set('110', 'z', 'will be deleted').commit()
+    bib = Bib().set('710', 'a', auth.id).set('710', 'z', auth.id).commit()
+    auth.heading_field.delete_subfield('z')
+    auth.commit()
+    bib.set('999', 'a', 'updated').commit()
+
+    q = Query.from_string('710:"will not be deleted"')
+    assert q.to_json()
\ No newline at end of file