diff --git a/src/blenderbim/blenderbim/core/document.py b/src/blenderbim/blenderbim/core/document.py index ce6dc374c6..85cfc00681 100644 --- a/src/blenderbim/blenderbim/core/document.py +++ b/src/blenderbim/blenderbim/core/document.py @@ -109,7 +109,7 @@ def remove_document(ifc, document_tool, document=None): def assign_document(ifc, product=None, document=None): - ifc.run("document.assign_document", product=product, document=document) + ifc.run("document.assign_document", products=[product], document=document) def unassign_document(ifc, product=None, document=None): diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index f8039488f2..127cbfa7db 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -241,7 +241,7 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None): attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"} ifc.run("document.edit_information", information=information, attributes=attributes) ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri}) - ifc.run("document.assign_document", product=element, document=reference) + ifc.run("document.assign_document", products=[element], document=reference) drawing.import_drawings() @@ -276,7 +276,7 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"} ifc.run("document.edit_information", information=information, attributes=attributes) ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri}) - ifc.run("document.assign_document", product=new_drawing, document=reference) + ifc.run("document.assign_document", products=[new_drawing], document=reference) drawing_tool.import_drawings() return new_drawing diff --git a/src/blenderbim/test/core/test_document.py b/src/blenderbim/test/core/test_document.py index 708c899e48..2349870d39 100644 --- a/src/blenderbim/test/core/test_document.py +++ b/src/blenderbim/test/core/test_document.py @@ -133,7 +133,7 @@ class TestRemoveDocument: class TestAssignDocument: def test_run(self, ifc): - ifc.run("document.assign_document", product="product", document="document").should_be_called() + ifc.run("document.assign_document", products=["product"], document="document").should_be_called() subject.assign_document(ifc, product="product", document="document") diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py index b07d0f8368..afe116d072 100644 --- a/src/blenderbim/test/core/test_drawing.py +++ b/src/blenderbim/test/core/test_drawing.py @@ -365,7 +365,7 @@ class TestAddDrawing: attributes={"Identification": "X", "Name": "name", "Scope": "DRAWING"}, ).should_be_called() ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called() - ifc.run("document.assign_document", product="element", document="reference").should_be_called() + ifc.run("document.assign_document", products=["element"], document="reference").should_be_called() drawing.import_drawings().should_be_called() subject.add_drawing(ifc, collector, drawing, target_view="target_view", location_hint="location_hint") @@ -405,7 +405,7 @@ class TestDuplicateDrawing: ifc.run( "document.edit_reference", reference="reference", attributes={"Location": "drawing_path"} ).should_be_called() - ifc.run("document.assign_document", product="new_drawing", document="reference").should_be_called() + ifc.run("document.assign_document", products=["new_drawing"], document="reference").should_be_called() drawing.import_drawings().should_be_called() subject.duplicate_drawing(ifc, drawing, drawing="drawing", should_duplicate_annotations=True) diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 6d27dbebfd..c82e076464 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -99,6 +99,9 @@ ARGUMENTS_DEPRECATION = { "library.unassign_reference": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "document.assign_document": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py index c7f98e4517..f67cbe890a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py @@ -17,11 +17,19 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element +from typing import Union class Usecase: - def __init__(self, file, product=None, document=None): - """Assigns a document to a product + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + document: ifcopenshell.entity_instance, + ): + """Assigns a document to a list of products An object may be assigned to zero, one, or multiple documents. Almost any object or property may be assigned to a document, though typically @@ -32,14 +40,16 @@ class Usecase: consistent with other external relationships (such as classification systems or libraries). - :param product: The object to associate the document to. This could be + :param product: The list of objects to associate the document to. This could be almost any sensible object in IFC. - :type product: ifcopenshell.entity_instance.entity_instance + :type product: list[ifcopenshell.entity_instance.entity_instance] :param document: The IfcDocumentReference to associate to, or alternatively an IfcDocumentInformation, though this is not recommended. :type document: ifcopenshell.entity_instance.entity_instance :return: The IfcRelAssociatesDocument relationship + or `None` if `products` was an empty list or all products were + already assigned to the `document`. :rtype: ifcopenshell.entity_instance.entity_instance Example: @@ -54,42 +64,51 @@ class Usecase: reference = ifcopenshell.api.run("document.add_reference", model, information=document) # Let's imagine storey represents an IfcBuildingStorey for the ground floor - ifcopenshell.api.run("document.assign_document", model, product=storey, document=reference) + ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference) """ self.file = file self.settings = { - "product": product, + "products": products, "document": document, } - def execute(self): - rel = self.get_document_rel() - related_objects = set(rel.RelatedObjects) if rel.RelatedObjects else set() - related_objects.add(self.settings["product"]) - rel.RelatedObjects = list(related_objects) + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? + # NOTE: reuses code from `library.assign_reference` + + referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["document"]) + products: set[ifcopenshell.entity_instance] = set(self.settings["products"]) + products = products - referenced_elements + + if not products: + return - def get_document_rel(self): if self.file.schema == "IFC2X3": - for rel in self.file.by_type("IfcRelAssociatesDocument"): - if rel.RelatingDocument == self.settings["document"]: - return rel + rel = next( + ( + r + for r in self.file.by_type("IfcRelAssociatesDocument") + if r.RelatingDocument == self.settings["document"] + ), + None, + ) else: - if ( - hasattr(self.settings["document"], "DocumentRefForObjects") - and self.settings["document"].DocumentRefForObjects - ): - return self.settings["document"].DocumentRefForObjects[0] - elif ( - hasattr(self.settings["document"], "DocumentInfoForObjects") - and self.settings["document"].DocumentInfoForObjects - ): - return self.settings["document"].DocumentInfoForObjects[0] + ifc_class = self.settings["document"].is_a() + if ifc_class == "IfcDocumentReference": + rel = next(iter(self.settings["document"].DocumentRefForObjects), None) + elif ifc_class == "IfcDocumentInformation": + rel = next(iter(self.settings["document"].DocumentInfoForObjects), None) - return self.file.create_entity( - "IfcRelAssociatesDocument", - **{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatingDocument": self.settings["document"], - } - ) + if not rel: + return self.file.create_entity( + "IfcRelAssociatesDocument", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), + RelatedObjects=list(products), + RelatingDocument=self.settings["document"], + ) + + related_objects = set(rel.RelatedObjects) | products + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, element=rel) + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py index 5a08c13262..c4fbca8234 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py @@ -45,7 +45,7 @@ class Usecase: reference = ifcopenshell.api.run("document.add_reference", model, information=document) # Let's imagine storey represents an IfcBuildingStorey for the ground floor - ifcopenshell.api.run("document.assign_document", model, product=storey, document=reference) + ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference) # Now let's change our mind and remove the association ifcopenshell.api.run("document.unassign_document", model, product=storey, document=reference) diff --git a/src/ifcopenshell-python/test/api/document/test_assign_document.py b/src/ifcopenshell-python/test/api/document/test_assign_document.py index 6c76f844b4..32d584ac51 100644 --- a/src/ifcopenshell-python/test/api/document/test_assign_document.py +++ b/src/ifcopenshell-python/test/api/document/test_assign_document.py @@ -18,22 +18,25 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestAssignDocument(test.bootstrap.IFC4): def test_assigning_a_document(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) assert element.HasAssociations[0].RelatingDocument == reference + assert ifcopenshell.util.element.get_referenced_elements(reference) == {element} def test_assigning_multiple_documents(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) - ifcopenshell.api.run("document.assign_document", self.file, product=element2, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element, element2], document=reference) assert len(self.file.by_type("IfcRelAssociatesDocument")) == 1 - assert element.HasAssociations[0].RelatingDocument == reference - assert element2.HasAssociations[0].RelatingDocument == reference - assert element.HasAssociations[0] == element.HasAssociations[0] + assert ifcopenshell.util.element.get_referenced_elements(reference) == {element, element2} + + +class TestAssignDocumentIFC2X3(test.bootstrap.IFC2X3, TestAssignDocument): + pass diff --git a/src/ifcopenshell-python/test/api/document/test_remove_reference.py b/src/ifcopenshell-python/test/api/document/test_remove_reference.py index e5f5892172..a1b3c260e2 100644 --- a/src/ifcopenshell-python/test/api/document/test_remove_reference.py +++ b/src/ifcopenshell-python/test/api/document/test_remove_reference.py @@ -35,7 +35,7 @@ class TestRemoveReference(test.bootstrap.IFC4): wall = self.file.createIfcWall() information = ifcopenshell.api.run("document.add_information", self.file, parent=None) reference = ifcopenshell.api.run("document.add_reference", self.file, information=information) - ifcopenshell.api.run("document.assign_document", self.file, product=wall, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[wall], document=reference) assert len(self.file.by_type("IfcRelAssociatesDocument")) == 2 ifcopenshell.api.run("document.remove_reference", self.file, reference=reference) assert len(self.file.by_type("IfcDocumentReference")) == 0 diff --git a/src/ifcopenshell-python/test/api/document/test_unassign_document.py b/src/ifcopenshell-python/test/api/document/test_unassign_document.py index 8f227dd2b2..f58975ce69 100644 --- a/src/ifcopenshell-python/test/api/document/test_unassign_document.py +++ b/src/ifcopenshell-python/test/api/document/test_unassign_document.py @@ -24,7 +24,7 @@ class TestUnassignDocument(test.bootstrap.IFC4): def test_unassigning_a_document(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference) assert not element.HasAssociations assert not len(self.file.by_type("IfcRelAssociatesDocument")) @@ -33,8 +33,8 @@ class TestUnassignDocument(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) - ifcopenshell.api.run("document.assign_document", self.file, product=element2, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element2], document=reference) ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference) assert not element.HasAssociations assert element2.HasAssociations[0].RelatingDocument == reference diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index a652a0de0c..66afb798a6 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -254,3 +254,10 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) ifcopenshell.api.run("library.unassign_reference", self.file, product=product, reference=reference) assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 + + @deprecation_check + def test_assigning_a_document(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) + ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) + assert element.HasAssociations[0].RelatingDocument == reference diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index 1a701f5ec9..97363f8017 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -787,7 +787,6 @@ class TestGetNestIFC2X3(test.bootstrap.IFC2X3, TestGetNestIFC4): class TestGetReferencedElements(test.bootstrap.IFC4): # TODO: test other references: - # IfcDocumentReference # IfcExternallyDefinedHatchStyle # IfcExternallyDefinedSurfaceStyle # IfcExternallyDefinedTextFont @@ -816,6 +815,15 @@ class TestGetReferencedElements(test.bootstrap.IFC4): ifcopenshell.api.run("library.assign_reference", self.file, reference=reference, products=elements) assert subject.get_referenced_elements(reference) == set(elements) + def test_get_elements_referenced_by_document_reference(self): + reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) + elements = [ + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ] + ifcopenshell.api.run("document.assign_document", self.file, document=reference, products=elements) + assert subject.get_referenced_elements(reference) == set(elements) + class TestGetReferencedElementsIFC2X3(test.bootstrap.IFC2X3, TestGetReferencedElements): pass