From beff90cd5e1dc4ccd3ed47fef3058ef971bac126 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 29 Sep 2021 18:39:10 +1000 Subject: [PATCH] Organisations now support null attributes. --- .../blenderbim/bim/module/owner/__init__.py | 3 - .../blenderbim/bim/module/owner/data.py | 127 +++++++---- .../blenderbim/bim/module/owner/operator.py | 110 ++------- .../blenderbim/bim/module/owner/prop.py | 18 +- .../blenderbim/bim/module/owner/ui.py | 210 +++++------------- src/blenderbim/blenderbim/core/owner.py | 23 ++ .../blenderbim/core/tool/__init__.py | 1 + .../core/tool/organisation_editor.py | 46 ++++ src/blenderbim/blenderbim/tool/__init__.py | 1 + .../blenderbim/tool/organisation_editor.py | 53 +++++ src/blenderbim/test/bim/bootstrap.py | 3 +- src/blenderbim/test/bim/feature/owner.feature | 35 +++ .../test/bim/module/void/test_operator.py | 2 +- src/blenderbim/test/core/bootstrap.py | 7 + src/blenderbim/test/core/test_owner.py | 36 ++- .../test/tool/test_organisation_editor.py | 94 ++++++++ 16 files changed, 454 insertions(+), 315 deletions(-) create mode 100644 src/blenderbim/blenderbim/core/tool/organisation_editor.py create mode 100644 src/blenderbim/blenderbim/tool/organisation_editor.py create mode 100644 src/blenderbim/test/tool/test_organisation_editor.py diff --git a/src/blenderbim/blenderbim/bim/module/owner/__init__.py b/src/blenderbim/blenderbim/bim/module/owner/__init__.py index 26e154171f..1cb236e8f9 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/owner/__init__.py @@ -20,7 +20,6 @@ import bpy from . import ui, prop, operator classes = ( - operator.AddOrRemoveElementFromCollection, operator.EnableEditingPerson, operator.DisableEditingPerson, operator.AddPerson, @@ -45,8 +44,6 @@ classes = ( operator.RemoveAddressAttribute, operator.EditAddress, operator.RemoveAddress, - prop.Person, - prop.Organisation, prop.BIMOwnerProperties, ui.BIM_PT_people, ui.BIM_PT_organisations, diff --git a/src/blenderbim/blenderbim/bim/module/owner/data.py b/src/blenderbim/blenderbim/bim/module/owner/data.py index b2f4f6dc91..c4bd2562f9 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/data.py +++ b/src/blenderbim/blenderbim/bim/module/owner/data.py @@ -20,7 +20,60 @@ import bpy import blenderbim.tool as tool -class PeopleData: +class RolesAddressesData: + @classmethod + def get_roles(cls, parent): + results = [] + for role in parent.Roles or []: + results.append( + { + "id": role.id(), + "is_editing": bpy.context.scene.BIMOwnerProperties.active_role_id == role.id(), + "label": role.UserDefinedRole or role.Role, + "props": bpy.context.scene.BIMOwnerProperties.role_attributes, + } + ) + return results + + @classmethod + def get_addresses(cls, parent): + results = [] + for address in parent.Addresses or []: + results.append( + { + "id": address.id(), + "is_editing": bpy.context.scene.BIMOwnerProperties.active_address_id == address.id(), + "label": address.is_a(), + "props": bpy.context.scene.BIMOwnerProperties.address_attributes, + "list_attributes": cls.get_address_list_attributes(address), + } + ) + return results + + @classmethod + def get_address_list_attributes(cls, address): + results = [] + props = bpy.context.scene.BIMOwnerProperties + if address.is_a("IfcPostalAddress"): + names = ["AddressLines"] + elif address.is_a("IfcTelecomAddress"): + names = ["TelephoneNumbers", "FacsimileNumbers", "ElectronicMailAddresses", "MessagingIDs"] + for name in names: + if name == "AddressLines": + items = [{"id": id, "prop": prop} for id, prop in enumerate(props.address_lines)] + elif name == "TelephoneNumbers": + items = [{"id": id, "prop": prop} for id, prop in enumerate(props.telephone_numbers)] + elif name == "FacsimileNumbers": + items = [{"id": id, "prop": prop} for id, prop in enumerate(props.facsimile_numbers)] + elif name == "ElectronicMailAddresses": + items = [{"id": id, "prop": prop} for id, prop in enumerate(props.electronic_mail_addresses)] + elif name == "MessagingIDs": + items = [{"id": id, "prop": prop} for id, prop in enumerate(props.messaging_ids)] + results.append({"name": name, "items": items}) + return results + + +class PeopleData(RolesAddressesData): data = {} is_loaded = False @@ -41,8 +94,8 @@ class PeopleData: "is_editing": cls.get_person_is_editing(person), "is_engaged": bool(person.EngagedIn), "list_attributes": cls.get_person_list_attributes(person), - "roles": cls.get_person_roles(person), - "addresses": cls.get_person_addresses(person), + "roles": cls.get_roles(person), + "addresses": cls.get_addresses(person), } ) return people @@ -77,53 +130,29 @@ class PeopleData: results.append({"name": name, "items": items}) return results - @classmethod - def get_person_roles(cls, person): - results = [] - for role in person.Roles or []: - results.append( - { - "id": role.id(), - "is_editing": bpy.context.scene.BIMOwnerProperties.active_role_id == role.id(), - "label": role.UserDefinedRole or role.Role, - "props": bpy.context.scene.BIMOwnerProperties.role_attributes, - } - ) - return results + +class OrganisationsData(RolesAddressesData): + data = {} + is_loaded = False @classmethod - def get_person_addresses(cls, person): - results = [] - for address in person.Addresses or []: - results.append( - { - "id": address.id(), - "is_editing": bpy.context.scene.BIMOwnerProperties.active_address_id == address.id(), - "label": address.is_a(), - "props": bpy.context.scene.BIMOwnerProperties.address_attributes, - "list_attributes": cls.get_address_list_attributes(address), - } - ) - return results + def load(cls): + cls.data = {"organisations": cls.get_organisations()} + cls.is_loaded = True @classmethod - def get_address_list_attributes(cls, address): - results = [] - props = bpy.context.scene.BIMOwnerProperties - if address.is_a("IfcPostalAddress"): - names = ["AddressLines"] - elif address.is_a("IfcTelecomAddress"): - names = ["TelephoneNumbers", "FacsimileNumbers", "ElectronicMailAddresses", "MessagingIDs"] - for name in names: - if name == "AddressLines": - items = [{"id": id, "prop": prop} for id, prop in enumerate(props.address_lines)] - elif name == "TelephoneNumbers": - items = [{"id": id, "prop": prop} for id, prop in enumerate(props.telephone_numbers)] - elif name == "FacsimileNumbers": - items = [{"id": id, "prop": prop} for id, prop in enumerate(props.facsimile_numbers)] - elif name == "ElectronicMailAddresses": - items = [{"id": id, "prop": prop} for id, prop in enumerate(props.electronic_mail_addresses)] - elif name == "MessagingIDs": - items = [{"id": id, "prop": prop} for id, prop in enumerate(props.messaging_ids)] - results.append({"name": name, "items": items}) - return results + def get_organisations(cls): + organisations = [] + for organisation in tool.Ifc().get().by_type("IfcOrganization"): + organisations.append( + { + "id": organisation.id(), + "props": bpy.context.scene.BIMOwnerProperties.organisation_attributes, + "name": organisation.Name, + "is_editing": bpy.context.scene.BIMOwnerProperties.active_organisation_id == organisation.id(), + "is_engaged": bool(organisation.Engages), + "roles": cls.get_roles(organisation), + "addresses": cls.get_addresses(organisation), + } + ) + return organisations diff --git a/src/blenderbim/blenderbim/bim/module/owner/operator.py b/src/blenderbim/blenderbim/bim/module/owner/operator.py index f1309afafe..d14fbf070b 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/operator.py +++ b/src/blenderbim/blenderbim/bim/module/owner/operator.py @@ -30,43 +30,7 @@ class Operator: def execute(self, context): IfcStore.execute_ifc_operator(self, context) blenderbim.bim.module.owner.data.PeopleData.is_loaded = False - return {"FINISHED"} - - -def flatten_collection(collection): - return [v.name for v in collection] if collection else None - - -def populate_collection(collection, collection_data): - collection.clear() - if collection_data: - for value in collection_data: - collection.add().name = value - else: - collection.add() - - -class AddOrRemoveElementFromCollection(bpy.types.Operator): - bl_idname = "bim.add_or_remove_element_from_collection" - bl_label = "Add or Remove Element From Collection" - bl_options = {"REGISTER", "UNDO"} - operation: bpy.props.EnumProperty( - items=(("+", "Add", "Add item to collection"), ("-", "Remove", "Remove item from collection")), - default="+", - ) - collection_path: bpy.props.StringProperty() - selected_item_idx: bpy.props.IntProperty(default=-1) - - def execute(self, context): - # Ugly but I hate using eval() - collection = context.scene - for attr in self.collection_path.split("."): - if hasattr(collection, attr): - collection = getattr(collection, attr) - if self.operation == "+" and hasattr(collection, "add"): - collection.add() - elif hasattr(collection, "remove") and 0 <= self.selected_item_idx < len(collection): - collection.remove(self.selected_item_idx) + blenderbim.bim.module.owner.data.OrganisationsData.is_loaded = False return {"FINISHED"} @@ -256,90 +220,50 @@ class RemoveAddress(bpy.types.Operator, Operator): core.remove_address(tool.Ifc(), address=tool.Ifc().get().by_id(self.address)) -class EnableEditingOrganisation(bpy.types.Operator): +class EnableEditingOrganisation(bpy.types.Operator, Operator): bl_idname = "bim.enable_editing_organisation" bl_label = "Enable Editing Organisation" bl_options = {"REGISTER", "UNDO"} - organisation_id: bpy.props.IntProperty() + organisation: bpy.props.IntProperty() - def execute(self, context): - self.file = IfcStore.get_file() - props = context.scene.BIMOwnerProperties - props.active_organisation_id = self.organisation_id - data = Data.organisations[self.organisation_id] - identification = data["Id"] if self.file.schema == "IFC2X3" else data["Identification"] - props.organisation.identification = identification or "" - props.organisation.name = data["Name"] - props.organisation.description = data["Description"] or "" - return {"FINISHED"} + def _execute(self, context): + core.enable_editing_organisation( + tool.OrganisationEditor, organisation=tool.Ifc().get().by_id(self.organisation) + ) -class DisableEditingOrganisation(bpy.types.Operator): +class DisableEditingOrganisation(bpy.types.Operator, Operator): bl_idname = "bim.disable_editing_organisation" bl_label = "Disable Editing Organisation" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - context.scene.BIMOwnerProperties.active_organisation_id = 0 - return {"FINISHED"} + def _execute(self, context): + core.disable_editing_organisation(tool.OrganisationEditor) -class AddOrganisation(bpy.types.Operator): +class AddOrganisation(bpy.types.Operator, Operator): bl_idname = "bim.add_organisation" bl_label = "Add Organisation" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - ifcopenshell.api.run("owner.add_organisation", IfcStore.get_file()) - Data.load(IfcStore.get_file()) - return {"FINISHED"} + core.add_organisation(tool.Ifc) -class EditOrganisation(bpy.types.Operator): +class EditOrganisation(bpy.types.Operator, Operator): bl_idname = "bim.edit_organisation" bl_label = "Edit Organisation" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - self.file = IfcStore.get_file() - props = context.scene.BIMOwnerProperties - attributes = { - "Identification": props.organisation.identification or None, - "Name": props.organisation.name, - "Description": props.organisation.description or None, - } - if self.file.schema == "IFC2X3": - attributes["Id"] = attributes["Identification"] - del attributes["Identification"] - ifcopenshell.api.run( - "owner.edit_organisation", - self.file, - **{"organisation": self.file.by_id(props.active_organisation_id), "attributes": attributes} - ) - Data.load(IfcStore.get_file()) - bpy.ops.bim.disable_editing_organisation() - return {"FINISHED"} + core.edit_organisation(tool.Ifc, tool.OrganisationEditor) -class RemoveOrganisation(bpy.types.Operator): +class RemoveOrganisation(bpy.types.Operator, Operator): bl_idname = "bim.remove_organisation" bl_label = "Remove Organisation" bl_options = {"REGISTER", "UNDO"} - organisation_id: bpy.props.IntProperty() - - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) + organisation: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "owner.remove_organisation", self.file, **{"organisation": self.file.by_id(self.organisation_id)} - ) - Data.load(IfcStore.get_file()) - return {"FINISHED"} + core.remove_organisation(tool.Ifc, tool.Ifc.get().by_id(self.organisation)) diff --git a/src/blenderbim/blenderbim/bim/module/owner/prop.py b/src/blenderbim/blenderbim/bim/module/owner/prop.py index 1acf5c36a1..4d8bbdc485 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/prop.py +++ b/src/blenderbim/blenderbim/bim/module/owner/prop.py @@ -67,30 +67,14 @@ def getOrganisations(self, context): return _organisations_enum -class Organisation(PropertyGroup): - identification: StringProperty(name="Identification") - name: StringProperty(name="Name") - description: StringProperty(name="Description") - - -class Person(PropertyGroup): - name: StringProperty(name="Identification") - family_name: StringProperty(name="Family Name") - given_name: StringProperty(name="Given Name") - middle_names: CollectionProperty(type=StrProperty, name="Middle Names") - prefix_titles: CollectionProperty(type=StrProperty, name="Prefixes") - suffix_titles: CollectionProperty(type=StrProperty, name="Suffixes") - - class BIMOwnerProperties(PropertyGroup): - person: PointerProperty(type=Person) person_attributes: CollectionProperty(name="Person Attributes", type=Attribute) middle_names: CollectionProperty(type=StrProperty, name="Middle Names") prefix_titles: CollectionProperty(type=StrProperty, name="Prefixes") suffix_titles: CollectionProperty(type=StrProperty, name="Suffixes") active_person_id: IntProperty(name="Active Person Id") - organisation: PointerProperty(type=Organisation) active_organisation_id: IntProperty(name="Active Organisation Id") + organisation_attributes: CollectionProperty(name="Organisation Attributes", type=Attribute) active_role_id: IntProperty(name="Active Role Id") role_attributes: CollectionProperty(name="Role Attributes", type=Attribute) active_address_id: IntProperty(name="Active Address Id") diff --git a/src/blenderbim/blenderbim/bim/module/owner/ui.py b/src/blenderbim/blenderbim/bim/module/owner/ui.py index f4a68ea92f..bd067ef4bf 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/ui.py +++ b/src/blenderbim/blenderbim/bim/module/owner/ui.py @@ -19,103 +19,70 @@ import bpy import blenderbim.bim.helper import blenderbim.tool as tool -from blenderbim.bim.module.owner.data import PeopleData +from blenderbim.bim.module.owner.data import PeopleData, OrganisationsData from bpy.types import Panel from ifcopenshell.api.owner.data import Data from blenderbim.bim.ifc import IfcStore -def draw_string_collection(layout, owner, collection_name): - column = layout.column(align=True) - collection = getattr(owner, collection_name) - for i in range(len(collection)): - if i == 0: - row = draw_prop_on_new_row( - column, collection[i], "name", align=True, text=f"{owner.bl_rna.properties[collection_name].name}" - ) - add_op = row.operator("bim.add_or_remove_element_from_collection", icon="ADD", text="") - add_op.operation = "+" - add_op.collection_path = collection.path_from_id() - else: - row = draw_prop_on_new_row(column, collection[i], "name", align=True, text=f"#{i + 1}") - rem_op = row.operator("bim.add_or_remove_element_from_collection", icon="REMOVE", text="") - rem_op.operation = "-" - rem_op.collection_path = collection.path_from_id() - rem_op.selected_item_idx = i - - def draw_prop_on_new_row(layout, owner, attribute, align=False, **kwargs): row = layout.row(align=align) row.prop(owner, attribute, **kwargs) return row -def draw_roles_ui(box, assigned_object_id, roles, context): - props = context.scene.BIMOwnerProperties +def draw_roles(box, parent): row = box.row(align=True) row.label(text="Roles") - row.operator("bim.add_role", icon="ADD", text="").assigned_object_id = assigned_object_id - for role_id in roles: - role = Data.roles[role_id] - if props.active_role_id == role_id: - blender_role = props.role - box2 = box.box() - row = draw_prop_on_new_row(box2, blender_role, "name", align=True, icon="MOD_CLOTH", text="") - row.operator("bim.edit_role", icon="CHECKMARK", text="") + op = row.operator("bim.add_role", icon="ADD", text="") + op.parent = parent["id"] + + for role in parent["roles"]: + if role["is_editing"]: + row = box.row(align=True) + row.operator("bim.edit_role", icon="CHECKMARK") row.operator("bim.disable_editing_role", icon="CANCEL", text="") - if blender_role.name == "USERDEFINED": - draw_prop_on_new_row(box2, blender_role, "user_defined_role") - draw_prop_on_new_row(box2, blender_role, "description") + blenderbim.bim.helper.draw_attributes(role["props"], box) else: row = box.row(align=True) - row.label(text=role["UserDefinedRole"] or role["Role"]) - row.operator("bim.enable_editing_role", icon="GREASEPENCIL", text="").role_id = role_id - row.operator("bim.remove_role", icon="X", text="").role_id = role_id + row.label(text=role["label"]) + row.operator("bim.enable_editing_role", icon="GREASEPENCIL", text="").role = role["id"] + row.operator("bim.remove_role", icon="X", text="").role = role["id"] -def draw_addresses_ui(box, assigned_object_id, addresses, file, context): - props = context.scene.BIMOwnerProperties +def draw_addresses(box, parent): row = box.row(align=True) row.label(text="Addresses") op = row.operator("bim.add_address", icon="LINK_BLEND", text="") - op.assigned_object_id = assigned_object_id + op.parent = parent["id"] op.ifc_class = "IfcTelecomAddress" op = row.operator("bim.add_address", icon="APPEND_BLEND", text="") - op.assigned_object_id = assigned_object_id + op.parent = parent["id"] op.ifc_class = "IfcPostalAddress" - for address_id in addresses: - address = Data.addresses[address_id] - if props.active_address_id == address_id: - blender_address = props.address - box2 = box.box() - row = draw_prop_on_new_row(box2, blender_address, "purpose", align=True, icon="MOD_CLOTH", text="") - row.operator("bim.edit_address", icon="CHECKMARK", text="") - row.operator("bim.disable_editing_address", icon="CANCEL", text="") - if blender_address.purpose == "USERDEFINED": - draw_prop_on_new_row(box2, blender_address, "user_defined_purpose") - draw_prop_on_new_row(box2, blender_address, "description") - if address["type"] == "IfcTelecomAddress": - draw_string_collection(box2, blender_address, "telephone_numbers") - draw_string_collection(box2, blender_address, "facsimile_numbers") - draw_prop_on_new_row(box2, blender_address, "pager_number") - draw_string_collection(box2, blender_address, "electronic_mail_addresses") - draw_prop_on_new_row(box2, blender_address, "www_home_page_url") - if file.schema != "IFC2X3": - draw_string_collection(box2, blender_address, "messaging_ids") - elif address["type"] == "IfcPostalAddress": - draw_prop_on_new_row(box2, blender_address, "internal_location") - draw_string_collection(box2, blender_address, "address_lines") - draw_prop_on_new_row(box2, blender_address, "postal_box") - draw_prop_on_new_row(box2, blender_address, "town") - draw_prop_on_new_row(box2, blender_address, "region") - draw_prop_on_new_row(box2, blender_address, "postal_code") - draw_prop_on_new_row(box2, blender_address, "country") + for address in parent["addresses"]: + if address["is_editing"]: + row = box.row(align=True) + row.operator("bim.edit_address", icon="CHECKMARK") + row.operator("bim.disable_editing_address", icon="CANCEL", text="") + blenderbim.bim.helper.draw_attributes(address["props"], box) + for attribute in address["list_attributes"]: + row = box.row(align=True) + row.label(text=attribute["name"]) + op = row.operator("bim.add_address_attribute", icon="ADD", text="") + op.name = attribute["name"] + + for item in attribute["items"]: + row = box.row(align=True) + row.prop(item["prop"], "name", text="") + op = row.operator("bim.remove_address_attribute", icon="REMOVE", text="") + op.name = attribute["name"] + op.id = item["id"] else: row = box.row(align=True) - row.label(text=address["type"]) - row.operator("bim.enable_editing_address", icon="GREASEPENCIL", text="").address_id = address_id - row.operator("bim.remove_address", icon="X", text="").address_id = address_id + row.label(text=address["label"]) + row.operator("bim.enable_editing_address", icon="GREASEPENCIL", text="").address = address["id"] + row.operator("bim.remove_address", icon="X", text="").address = address["id"] class BIM_PT_people(bpy.types.Panel): @@ -164,8 +131,8 @@ class BIM_PT_people(bpy.types.Panel): op.name = attribute["name"] op.id = item["id"] - self.draw_roles(box, person) - self.draw_addresses(box, person) + draw_roles(box, person) + draw_addresses(box, person) else: row = self.layout.row(align=True) row.label(text=person["name"]) @@ -173,58 +140,6 @@ class BIM_PT_people(bpy.types.Panel): if not person["is_engaged"]: row.operator("bim.remove_person", icon="X", text="").person = person["id"] - def draw_roles(self, box, person): - row = box.row(align=True) - row.label(text="Roles") - op = row.operator("bim.add_role", icon="ADD", text="") - op.parent = person["id"] - - for role in person["roles"]: - if role["is_editing"]: - row = box.row(align=True) - row.operator("bim.edit_role", icon="CHECKMARK") - row.operator("bim.disable_editing_role", icon="CANCEL", text="") - blenderbim.bim.helper.draw_attributes(role["props"], box) - else: - row = box.row(align=True) - row.label(text=role["label"]) - row.operator("bim.enable_editing_role", icon="GREASEPENCIL", text="").role = role["id"] - row.operator("bim.remove_role", icon="X", text="").role = role["id"] - - def draw_addresses(self, box, person): - row = box.row(align=True) - row.label(text="Addresses") - op = row.operator("bim.add_address", icon="LINK_BLEND", text="") - op.parent = person["id"] - op.ifc_class = "IfcTelecomAddress" - op = row.operator("bim.add_address", icon="APPEND_BLEND", text="") - op.parent = person["id"] - op.ifc_class = "IfcPostalAddress" - - for address in person["addresses"]: - if address["is_editing"]: - row = box.row(align=True) - row.operator("bim.edit_address", icon="CHECKMARK") - row.operator("bim.disable_editing_address", icon="CANCEL", text="") - blenderbim.bim.helper.draw_attributes(address["props"], box) - for attribute in address["list_attributes"]: - row = box.row(align=True) - row.label(text=attribute["name"]) - op = row.operator("bim.add_address_attribute", icon="ADD", text="") - op.name = attribute["name"] - - for item in attribute["items"]: - row = box.row(align=True) - row.prop(item["prop"], "name", text="") - op = row.operator("bim.remove_address_attribute", icon="REMOVE", text="") - op.name = attribute["name"] - op.id = item["id"] - else: - row = box.row(align=True) - row.label(text=address["label"]) - row.operator("bim.enable_editing_address", icon="GREASEPENCIL", text="").address = address["id"] - row.operator("bim.remove_address", icon="X", text="").address = address["id"] - class BIM_PT_organisations(Panel): bl_label = "IFC Organisations" @@ -239,40 +154,35 @@ class BIM_PT_organisations(Panel): return IfcStore.get_file() def draw(self, context): - if not Data.is_loaded: - Data.load(IfcStore.get_file()) + if not OrganisationsData.is_loaded: + OrganisationsData.load() - self.file = IfcStore.get_file() self.layout.use_property_split = True self.layout.use_property_decorate = False - props = context.scene.BIMOwnerProperties row = self.layout.row() row.operator("bim.add_organisation", icon="ADD") - for organisation_id, organisation in Data.organisations.items(): - if props.active_organisation_id == organisation_id: - blender_organisation = props.organisation - box = self.layout.box() - row = box.row(align=True) - row.prop(blender_organisation, "name", icon="USER", text="") - row.operator("bim.edit_organisation", icon="CHECKMARK", text="") - row.operator("bim.disable_editing_organisation", icon="CANCEL", text="") - draw_prop_on_new_row(box, blender_organisation, "identification") - draw_prop_on_new_row(box, blender_organisation, "description") + for organisation in OrganisationsData.data["organisations"]: + self.draw_organisation(organisation) - draw_roles_ui(box, organisation_id, organisation["Roles"], context) - draw_addresses_ui(box, organisation_id, organisation["Addresses"], self.file, context) - else: - row = self.layout.row(align=True) - row.label(text=organisation["Name"]) - if organisation["Roles"]: - row.label(text=", ".join([Data.roles[r]["Role"] for r in organisation["Roles"]])) - row.operator( - "bim.enable_editing_organisation", icon="GREASEPENCIL", text="" - ).organisation_id = organisation_id - if not organisation["is_engaged"]: - row.operator("bim.remove_organisation", icon="X", text="").organisation_id = organisation_id + def draw_organisation(self, organisation): + if organisation["is_editing"]: + box = self.layout.box() + row = box.row(align=True) + row.operator("bim.edit_organisation", icon="CHECKMARK") + row.operator("bim.disable_editing_organisation", icon="CANCEL", text="") + blenderbim.bim.helper.draw_attributes(organisation["props"], box) + + draw_roles(box, organisation) + draw_addresses(box, organisation) + else: + row = self.layout.row(align=True) + row.label(text=organisation["name"]) + op = row.operator("bim.enable_editing_organisation", icon="GREASEPENCIL", text="") + op.organisation = organisation["id"] + if not organisation["is_engaged"]: + row.operator("bim.remove_organisation", icon="X", text="").organisation = organisation["id"] class BIM_PT_owner(Panel): diff --git a/src/blenderbim/blenderbim/core/owner.py b/src/blenderbim/blenderbim/core/owner.py index d30e5c3cee..4c89c2dcc4 100644 --- a/src/blenderbim/blenderbim/core/owner.py +++ b/src/blenderbim/blenderbim/core/owner.py @@ -98,3 +98,26 @@ def add_address_attribute(address_editor, name=None): def remove_address_attribute(address_editor, name=None, id=None): address_editor.remove_attribute(name, id) + + +def add_organisation(ifc): + return ifc.run("owner.add_organisation") + + +def remove_organisation(ifc, organisation=None): + ifc.run("owner.remove_organisation", organisation=organisation) + + +def enable_editing_organisation(organisation_editor, organisation=None): + organisation_editor.set_organisation(organisation) + organisation_editor.import_attributes() + + +def disable_editing_organisation(organisation_editor): + organisation_editor.clear_organisation() + + +def edit_organisation(ifc, organisation_editor): + organisation = organisation_editor.get_organisation() + ifc.run("owner.edit_organisation", organisation=organisation, attributes=organisation_editor.export_attributes()) + organisation_editor.clear_organisation() diff --git a/src/blenderbim/blenderbim/core/tool/__init__.py b/src/blenderbim/blenderbim/core/tool/__init__.py index 09f78918e7..64a7bf69fe 100644 --- a/src/blenderbim/blenderbim/core/tool/__init__.py +++ b/src/blenderbim/blenderbim/core/tool/__init__.py @@ -21,3 +21,4 @@ from blenderbim.core.tool.blender import Blender from blenderbim.core.tool.person_editor import PersonEditor from blenderbim.core.tool.role_editor import RoleEditor from blenderbim.core.tool.address_editor import AddressEditor +from blenderbim.core.tool.organisation_editor import OrganisationEditor diff --git a/src/blenderbim/blenderbim/core/tool/organisation_editor.py b/src/blenderbim/blenderbim/core/tool/organisation_editor.py new file mode 100644 index 0000000000..85d73f8324 --- /dev/null +++ b/src/blenderbim/blenderbim/core/tool/organisation_editor.py @@ -0,0 +1,46 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import abc + + +class OrganisationEditor(abc.ABC): + @classmethod + @abc.abstractmethod + def set_organisation(cls, organisation): + pass + + @classmethod + @abc.abstractmethod + def import_attributes(cls): + pass + + @classmethod + @abc.abstractmethod + def clear_organisation(cls): + pass + + @classmethod + @abc.abstractmethod + def get_organisation(cls): + pass + + @classmethod + @abc.abstractmethod + def export_attributes(cls): + pass diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index e330be0cdf..bc746d2f98 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -21,3 +21,4 @@ from blenderbim.tool.blender import Blender from blenderbim.tool.person_editor import PersonEditor from blenderbim.tool.role_editor import RoleEditor from blenderbim.tool.address_editor import AddressEditor +from blenderbim.tool.organisation_editor import OrganisationEditor diff --git a/src/blenderbim/blenderbim/tool/organisation_editor.py b/src/blenderbim/blenderbim/tool/organisation_editor.py new file mode 100644 index 0000000000..0764447727 --- /dev/null +++ b/src/blenderbim/blenderbim/tool/organisation_editor.py @@ -0,0 +1,53 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import ifcopenshell.api +import blenderbim.core.tool +import blenderbim.bim.helper +import blenderbim.tool as tool + + +class OrganisationEditor(blenderbim.core.tool.organisation_editor.OrganisationEditor): + @classmethod + def set_organisation(cls, organisation): + bpy.context.scene.BIMOwnerProperties.active_organisation_id = organisation.id() + + @classmethod + def import_attributes(cls): + organisation = tool.Ifc.get().by_id(bpy.context.scene.BIMOwnerProperties.active_organisation_id) + props = bpy.context.scene.BIMOwnerProperties + props.organisation_attributes.clear() + + blenderbim.bim.helper.import_attributes( + "IfcOrganization", props.organisation_attributes, organisation.get_info() + ) + + @classmethod + def clear_organisation(cls): + bpy.context.scene.BIMOwnerProperties.active_organisation_id = 0 + + @classmethod + def export_attributes(cls): + props = bpy.context.scene.BIMOwnerProperties + attributes = blenderbim.bim.helper.export_attributes(props.organisation_attributes) + return attributes + + @classmethod + def get_organisation(cls): + return tool.Ifc().get().by_id(bpy.context.scene.BIMOwnerProperties.active_organisation_id) diff --git a/src/blenderbim/test/bim/bootstrap.py b/src/blenderbim/test/bim/bootstrap.py index 5071210569..146033b89a 100644 --- a/src/blenderbim/test/bim/bootstrap.py +++ b/src/blenderbim/test/bim/bootstrap.py @@ -95,6 +95,7 @@ def additionally_the_object_name_is_selected(name): bpy.context.view_layer.objects.active = obj obj.select_set(True) + def i_deselect_all_objects(): bpy.context.view_layer.objects.active = None bpy.ops.object.select_all(action="DESELECT") @@ -346,7 +347,7 @@ definitions = { 'I add a cube of size "([0-9]+)" at "(.*)"': i_add_a_cube_of_size_size_at_location, 'the object "(.*)" is selected': the_object_name_is_selected, 'additionally the object "(.*)" is selected': additionally_the_object_name_is_selected, - 'I deselect all objects': i_deselect_all_objects, + "I deselect all objects": i_deselect_all_objects, 'I am on frame "([0-9]+)"': i_am_on_frame_number, 'I set "(.*)" to "(.*)"': i_set_prop_to_value, '"(.*)" is "(.*)"': prop_is_value, diff --git a/src/blenderbim/test/bim/feature/owner.feature b/src/blenderbim/test/bim/feature/owner.feature index c5183bb730..7313dcf346 100644 --- a/src/blenderbim/test/bim/feature/owner.feature +++ b/src/blenderbim/test/bim/feature/owner.feature @@ -165,3 +165,38 @@ Scenario: Edit address And I press "bim.enable_editing_address(address={address})" And I press "bim.edit_address()" Then nothing happens + +Scenario: Add organisation + Given an empty IFC project + When I press "bim.add_organisation" + Then nothing happens + +Scenario: Enable editing organisation + Given an empty IFC project + When I press "bim.add_organisation" + And the variable "organisation" is "{ifc}.by_type('IfcOrganization')[0].id()" + And I press "bim.enable_editing_organisation(organisation={organisation})" + Then "scene.BIMOwnerProperties.active_organisation_id" is "{organisation}" + +Scenario: Disable editing organisation + Given an empty IFC project + When I press "bim.add_organisation" + And the variable "organisation" is "{ifc}.by_type('IfcOrganization')[0].id()" + And I press "bim.enable_editing_organisation(organisation={organisation})" + And I press "bim.disable_editing_organisation" + Then "scene.BIMOwnerProperties.active_organisation_id" is "0" + +Scenario: Edit organisation + Given an empty IFC project + When I press "bim.add_organisation" + And the variable "organisation" is "{ifc}.by_type('IfcOrganization')[0].id()" + And I press "bim.enable_editing_organisation(organisation={organisation})" + And I press "bim.edit_organisation" + Then "scene.BIMOwnerProperties.active_organisation_id" is "0" + +Scenario: Remove organisation + Given an empty IFC project + When I press "bim.add_organisation" + And the variable "organisation" is "{ifc}.by_type('IfcOrganization')[0].id()" + And I press "bim.remove_organisation(organisation={organisation})" + Then nothing happens diff --git a/src/blenderbim/test/bim/module/void/test_operator.py b/src/blenderbim/test/bim/module/void/test_operator.py index 742678d50b..b5aa4b7863 100644 --- a/src/blenderbim/test/bim/module/void/test_operator.py +++ b/src/blenderbim/test/bim/module/void/test_operator.py @@ -382,7 +382,7 @@ class TestRemoveFilling(test.bim.bootstrap.NewFile): And I delete the selected objects Then the object "IfcOpeningElement/Cube" is an "IfcOpeningElement" And the void "IfcOpeningElement/Cube" is not filled by "Cube" - """ + """ @test.bim.bootstrap.scenario def test_removing_a_filling_using_deletion_on_the_opening(self): diff --git a/src/blenderbim/test/core/bootstrap.py b/src/blenderbim/test/core/bootstrap.py index 353e79df41..7e0161123a 100644 --- a/src/blenderbim/test/core/bootstrap.py +++ b/src/blenderbim/test/core/bootstrap.py @@ -56,6 +56,13 @@ def address_editor(): prophet.verify() +@pytest.fixture +def organisation_editor(): + prophet = Prophecy(blenderbim.core.tool.OrganisationEditor) + yield prophet + prophet.verify() + + class Prophecy: def __init__(self, cls): self.subject = cls diff --git a/src/blenderbim/test/core/test_owner.py b/src/blenderbim/test/core/test_owner.py index 85cf485f9b..bc4cca86e5 100644 --- a/src/blenderbim/test/core/test_owner.py +++ b/src/blenderbim/test/core/test_owner.py @@ -18,7 +18,7 @@ import blenderbim.core.owner as subject -from test.core.bootstrap import ifc, blender, person_editor, role_editor, address_editor +from test.core.bootstrap import ifc, blender, person_editor, role_editor, address_editor, organisation_editor class TestAddPerson: @@ -147,3 +147,37 @@ class TestRemoveAddressAttribute: def test_run(self, address_editor): address_editor.remove_attribute("name", "id").should_be_called() subject.remove_address_attribute(address_editor, name="name", id="id") + + +class TestAddOrganisation: + def test_run(self, ifc): + ifc.run("owner.add_organisation").should_be_called().will_return("organisation") + assert subject.add_organisation(ifc) == "organisation" + + +class TestRemoveOrganisation: + def test_run(self, ifc): + ifc.run("owner.remove_organisation", organisation="organisation").should_be_called() + subject.remove_organisation(ifc, organisation="organisation") + + +class TestEnableEditingOrganisation: + def test_run(self, organisation_editor): + organisation_editor.set_organisation("organisation").should_be_called() + organisation_editor.import_attributes().should_be_called() + subject.enable_editing_organisation(organisation_editor, organisation="organisation") + + +class TestDisableEditingOrganisation: + def test_run(self, organisation_editor): + organisation_editor.clear_organisation().should_be_called() + subject.disable_editing_organisation(organisation_editor) + + +class TestEditOrganisation: + def test_run(self, ifc, organisation_editor): + organisation_editor.get_organisation().should_be_called().will_return("organisation") + organisation_editor.export_attributes().should_be_called().will_return("attributes") + ifc.run("owner.edit_organisation", organisation="organisation", attributes="attributes").should_be_called() + organisation_editor.clear_organisation().should_be_called() + subject.edit_organisation(ifc, organisation_editor) diff --git a/src/blenderbim/test/tool/test_organisation_editor.py b/src/blenderbim/test/tool/test_organisation_editor.py new file mode 100644 index 0000000000..6b4d338ea7 --- /dev/null +++ b/src/blenderbim/test/tool/test_organisation_editor.py @@ -0,0 +1,94 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import ifcopenshell +import test.bim.bootstrap +import blenderbim.core.tool +import blenderbim.tool as tool +from blenderbim.tool.organisation_editor import OrganisationEditor as subject + + +class TestImplementsTool(test.bim.bootstrap.NewFile): + def test_run(self): + assert isinstance(subject(), blenderbim.core.tool.organisation_editor.OrganisationEditor) + + +class TestSetOrganisation(test.bim.bootstrap.NewFile): + def test_run(self): + organisation = ifcopenshell.file().createIfcOrganization() + subject().set_organisation(organisation) + assert bpy.context.scene.BIMOwnerProperties.active_organisation_id == organisation.id() + + +class TestImportAttributes(test.bim.bootstrap.NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + organisation = ifc.createIfcOrganization() + organisation.Identification = "Identification" + organisation.Name = "Name" + organisation.Description = "Description" + subject().set_organisation(organisation) + subject().import_attributes() + props = bpy.context.scene.BIMOwnerProperties + assert props.organisation_attributes.get("Identification").string_value == "Identification" + assert props.organisation_attributes.get("Name").string_value == "Name" + assert props.organisation_attributes.get("Description").string_value == "Description" + + def test_overwriting_a_previous_import(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + organisation = ifc.createIfcOrganization() + organisation.Identification = "Identification" + organisation.Description = "Description" + subject().set_organisation(organisation) + subject().import_attributes() + organisation.Identification = "Identification2" + organisation.Description = None + subject().import_attributes() + props = bpy.context.scene.BIMOwnerProperties + assert props.organisation_attributes.get("Identification").string_value == "Identification2" + assert props.organisation_attributes.get("Description").string_value == "" + + +class TestClearOrganisation(test.bim.bootstrap.NewFile): + def test_run(self): + props = bpy.context.scene.BIMOwnerProperties + props.active_organisation_id = 1 + subject().clear_organisation() + assert props.active_organisation_id == 0 + + +class TestExportAttributes(test.bim.bootstrap.NewFile): + def test_run(self): + TestImportAttributes().test_run() + assert subject().export_attributes() == { + "Identification": "Identification", + "Name": "Name", + "Description": "Description", + } + + +class TestGetOrganisation(test.bim.bootstrap.NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + organisation = ifc.createIfcOrganization() + subject().set_organisation(organisation) + assert subject().get_organisation() == organisation