def global_stack(): gs = GlobalStack("test_global_stack") gs._metadata = { "supported_actions": ["supported_action_1", "supported_action_2"], "required_actions": ["required_action_1", "required_action_2"], "first_start_actions": ["first_start_actions_1", "first_start_actions_2"] } return gs
def _connectByNetworkKey(self, active_machine: GlobalStack) -> None: # Check if the active printer has a local network connection and match this key to the remote cluster. local_network_key = active_machine.getMetaDataEntry("um_network_key") if not local_network_key: return device = next((c for c in self._remote_clusters.values() if c.matchesNetworkKey(local_network_key)), None) if not device: return Logger.log("i", "Found cluster %s with network key %s", device, local_network_key) active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) self._connectToOutputDevice(device, active_machine)
def _setOutputDeviceMetadata(self, device: CloudOutputDevice, machine: GlobalStack): machine.setName(device.name) machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) machine.setMetaDataEntry(self.META_HOST_GUID, device.clusterData.host_guid) machine.setMetaDataEntry("group_name", device.name) machine.setMetaDataEntry("group_size", device.clusterSize) digital_factory_string = self.I18N_CATALOG.i18nc( "info:name", "Ultimaker Digital Factory") digital_factory_link = "<a href='https://digitalfactory.ultimaker.com/'>{}</a>".format( digital_factory_string) removal_warning_string = self.I18N_CATALOG.i18nc( "@label ({printer_name} is replaced with the name of the printer", "{printer_name} will be removed until the next account sync. <br> To remove {printer_name} permanently, " "visit {digital_factory_link}" "<br><br>Are you sure you want to remove {printer_name} temporarily?" .format(printer_name=device.name, digital_factory_link=digital_factory_link)) machine.setMetaDataEntry("removal_warning", removal_warning_string) machine.addConfiguredConnectionType(device.connectionType.value)
def _connectToOutputDevice(self, device: CloudOutputDevice, active_machine: GlobalStack) -> None: device.connect() self._output_device_manager.addOutputDevice(device) active_machine.addConfiguredConnectionType(device.connectionType.value)
def test_getPropertyFallThrough(extruder_stack): # ExtruderStack.setNextStack calls registerExtruder for backward compatibility, but we do not need a complete extruder manager ExtruderManager._ExtruderManager__instance = unittest.mock.MagicMock() #A few instance container mocks to put in the stack. mock_layer_heights = { } #For each container type, a mock container that defines layer height to something unique. mock_no_settings = { } #For each container type, a mock container that has no settings at all. container_indices = cura.Settings.CuraContainerStack._ContainerIndexes #Cache. for type_id, type_name in container_indices.IndexTypeMap.items(): container = unittest.mock.MagicMock() # Return type_id when asking for value and -1 when asking for settable_per_extruder container.getProperty = lambda key, property, context=None, type_id=type_id: type_id if ( key == "layer_height" and property == "value" ) else ( None if property != "settable_per_extruder" else "-1" ) #Returns the container type ID as layer height, in order to identify it. container.hasProperty = lambda key, property: key == "layer_height" container.getMetaDataEntry = unittest.mock.MagicMock( return_value=type_name) mock_layer_heights[type_id] = container container = unittest.mock.MagicMock() container.getProperty = unittest.mock.MagicMock( return_value=None) #Has no settings at all. container.hasProperty = unittest.mock.MagicMock(return_value=False) container.getMetaDataEntry = unittest.mock.MagicMock( return_value=type_name) mock_no_settings[type_id] = container extruder_stack.userChanges = mock_no_settings[ container_indices.UserChanges] extruder_stack.qualityChanges = mock_no_settings[ container_indices.QualityChanges] extruder_stack.quality = mock_no_settings[container_indices.Quality] extruder_stack.material = mock_no_settings[container_indices.Material] extruder_stack.variant = mock_no_settings[container_indices.Variant] with unittest.mock.patch( "cura.Settings.CuraContainerStack.DefinitionContainer", unittest.mock.MagicMock): #To guard against the type checking. extruder_stack.definition = mock_layer_heights[ container_indices.Definition] #There's a layer height in here! stack = GlobalStack("PyTest GlobalStack") extruder_stack.setNextStack(stack) assert extruder_stack.getProperty("layer_height", "value") == container_indices.Definition extruder_stack.variant = mock_layer_heights[container_indices.Variant] assert extruder_stack.getProperty("layer_height", "value") == container_indices.Variant extruder_stack.material = mock_layer_heights[container_indices.Material] assert extruder_stack.getProperty("layer_height", "value") == container_indices.Material extruder_stack.quality = mock_layer_heights[container_indices.Quality] assert extruder_stack.getProperty("layer_height", "value") == container_indices.Quality extruder_stack.qualityChanges = mock_layer_heights[ container_indices.QualityChanges] assert extruder_stack.getProperty( "layer_height", "value") == container_indices.QualityChanges extruder_stack.userChanges = mock_layer_heights[ container_indices.UserChanges] assert extruder_stack.getProperty("layer_height", "value") == container_indices.UserChanges
def _setOutputDeviceMetadata(self, device: CloudOutputDevice, machine: GlobalStack): machine.setName(device.name) machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) machine.setMetaDataEntry(self.META_HOST_GUID, device.clusterData.host_guid) machine.setMetaDataEntry("group_name", device.name) machine.setMetaDataEntry("group_size", device.clusterSize) machine.setMetaDataEntry("removal_warning", self.I18N_CATALOG.i18nc( "@label ({} is printer name)", "{} will be removed until the next account sync. <br> To remove {} permanently, " "visit <a href='https://mycloud.ultimaker.com/'>Ultimaker Digital Factory</a>. " "<br><br>Are you sure you want to remove {} temporarily?", device.name, device.name, device.name )) machine.addConfiguredConnectionType(device.connectionType.value)
def global_stack(definition_changes_container) -> GlobalStack: global_stack = GlobalStack("TestGlobalStack") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.DefinitionChanges] = definition_changes_container return global_stack
def read(self, file_name): archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] # Create a shadow copy of the preferences (we don't want all of the preferences, but we do want to re-use its # parsing code. temp_preferences = Preferences() temp_preferences.readFromFile(io.TextIOWrapper(archive.open("Cura/preferences.cfg"))) # We need to wrap it, else the archive parser breaks. # Copy a number of settings from the temp preferences to the global global_preferences = Preferences.getInstance() visible_settings = temp_preferences.getValue("general/visible_settings") if visible_settings is None: Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged") else: global_preferences.setValue("general/visible_settings", visible_settings) categories_expanded = temp_preferences.getValue("cura/categories_expanded") if categories_expanded is None: Logger.log("w", "Workspace did not contain expanded categories. Leaving them unchanged") else: global_preferences.setValue("cura/categories_expanded", categories_expanded) Application.getInstance().expandedCategoriesChanged.emit() # Notify the GUI of the change self._id_mapping = {} # We don't add containers right away, but wait right until right before the stack serialization. # We do this so that if something goes wrong, it's easier to clean up. containers_to_add = [] global_stack_file, extruder_stack_files = self._determineGlobalAndExtruderStackFiles(file_name, cura_file_names) global_stack = None extruder_stacks = [] extruder_stacks_added = [] container_stacks_added = [] containers_added = [] global_stack_id_original = self._stripFileToId(global_stack_file) global_stack_id_new = global_stack_id_original global_stack_need_rename = False extruder_stack_id_map = {} # new and old ExtruderStack IDs map if self._resolve_strategies["machine"] == "new": # We need a new id if the id already exists if self._container_registry.findContainerStacks(id = global_stack_id_original): global_stack_id_new = self.getNewId(global_stack_id_original) global_stack_need_rename = True for each_extruder_stack_file in extruder_stack_files: old_container_id = self._stripFileToId(each_extruder_stack_file) new_container_id = old_container_id if self._container_registry.findContainerStacks(id = old_container_id): # get a new name for this extruder new_container_id = self.getNewId(old_container_id) extruder_stack_id_map[old_container_id] = new_container_id # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few # TODO: cases that the container loaded is the same (most notable in materials & definitions). # TODO: It might be possible that we need to add smarter checking in the future. Logger.log("d", "Workspace loading is checking definitions...") # Get all the definition files & check if they exist. If not, add them. definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] for definition_container_file in definition_container_files: container_id = self._stripFileToId(definition_container_file) definitions = self._container_registry.findDefinitionContainers(id = container_id) if not definitions: definition_container = DefinitionContainer(container_id) definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8")) self._container_registry.addContainer(definition_container) Job.yieldThread() Logger.log("d", "Workspace loading is checking materials...") material_containers = [] # Get all the material files and check if they exist. If not, add them. xml_material_profile = self._getXmlProfileClass() if self._material_container_suffix is None: self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] if xml_material_profile: material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)] for material_container_file in material_container_files: container_id = self._stripFileToId(material_container_file) materials = self._container_registry.findInstanceContainers(id = container_id) if not materials: material_container = xml_material_profile(container_id) material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) containers_to_add.append(material_container) else: material_container = materials[0] if not material_container.isReadOnly(): # Only create new materials if they are not read only. if self._resolve_strategies["material"] == "override": material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) elif self._resolve_strategies["material"] == "new": # Note that we *must* deserialize it with a new ID, as multiple containers will be # auto created & added. material_container = xml_material_profile(self.getNewId(container_id)) material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) containers_to_add.append(material_container) material_containers.append(material_container) Job.yieldThread() Logger.log("d", "Workspace loading is checking instance containers...") # Get quality_changes and user profiles saved in the workspace instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] user_instance_containers = [] quality_and_definition_changes_instance_containers = [] for instance_container_file in instance_container_files: container_id = self._stripFileToId(instance_container_file) serialized = archive.open(instance_container_file).read().decode("utf-8") # HACK! we ignore "quality" and "variant" instance containers! parser = configparser.ConfigParser() parser.read_string(serialized) if not parser.has_option("metadata", "type"): Logger.log("w", "Cannot find metadata/type in %s, ignoring it", instance_container_file) continue if parser.get("metadata", "type") in self._ignored_instance_container_types: continue instance_container = InstanceContainer(container_id) # Deserialize InstanceContainer by converting read data from bytes to string instance_container.deserialize(serialized) container_type = instance_container.getMetaDataEntry("type") Job.yieldThread() # # IMPORTANT: # If an instance container (or maybe other type of container) exists, and user chooses "Create New", # we need to rename this container and all references to it, and changing those references are VERY # HARD. # if container_type in self._ignored_instance_container_types: # Ignore certain instance container types Logger.log("w", "Ignoring instance container [%s] with type [%s]", container_id, container_type) continue elif container_type == "user": # Check if quality changes already exists. user_containers = self._container_registry.findInstanceContainers(id = container_id) if not user_containers: containers_to_add.append(instance_container) else: if self._resolve_strategies["machine"] == "override" or self._resolve_strategies["machine"] is None: instance_container = user_containers[0] instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) instance_container.setDirty(True) elif self._resolve_strategies["machine"] == "new": # The machine is going to get a spiffy new name, so ensure that the id's of user settings match. old_extruder_id = instance_container.getMetaDataEntry("extruder", None) if old_extruder_id: new_extruder_id = extruder_stack_id_map[old_extruder_id] new_id = new_extruder_id + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) instance_container.setMetaDataEntry("extruder", new_extruder_id) containers_to_add.append(instance_container) machine_id = instance_container.getMetaDataEntry("machine", None) if machine_id: new_machine_id = self.getNewId(machine_id) new_id = new_machine_id + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) instance_container.setMetaDataEntry("machine", new_machine_id) containers_to_add.append(instance_container) user_instance_containers.append(instance_container) elif container_type in ("quality_changes", "definition_changes"): # Check if quality changes already exists. changes_containers = self._container_registry.findInstanceContainers(id = container_id) if not changes_containers: # no existing containers with the same ID, so we can safely add the new one containers_to_add.append(instance_container) else: # we have found existing container with the same ID, so we need to resolve according to the # selected strategy. if self._resolve_strategies[container_type] == "override": instance_container = changes_containers[0] instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) instance_container.setDirty(True) elif self._resolve_strategies[container_type] == "new": # TODO: how should we handle the case "new" for quality_changes and definition_changes? instance_container.setName(self._container_registry.uniqueName(instance_container.getName())) new_changes_container_id = self.getNewId(instance_container.getId()) instance_container._id = new_changes_container_id # TODO: we don't know the following is correct or not, need to verify # AND REFACTOR!!! if self._resolve_strategies["machine"] == "new": # The machine is going to get a spiffy new name, so ensure that the id's of user settings match. old_extruder_id = instance_container.getMetaDataEntry("extruder", None) if old_extruder_id: new_extruder_id = extruder_stack_id_map[old_extruder_id] instance_container.setMetaDataEntry("extruder", new_extruder_id) machine_id = instance_container.getMetaDataEntry("machine", None) if machine_id: new_machine_id = self.getNewId(machine_id) instance_container.setMetaDataEntry("machine", new_machine_id) containers_to_add.append(instance_container) elif self._resolve_strategies[container_type] is None: # The ID already exists, but nothing in the values changed, so do nothing. pass quality_and_definition_changes_instance_containers.append(instance_container) else: existing_container = self._container_registry.findInstanceContainers(id = container_id) if not existing_container: containers_to_add.append(instance_container) if global_stack_need_rename: if instance_container.getMetaDataEntry("machine"): instance_container.setMetaDataEntry("machine", global_stack_id_new) # Add all the containers right before we try to add / serialize the stack for container in containers_to_add: self._container_registry.addContainer(container) container.setDirty(True) containers_added.append(container) # Get the stack(s) saved in the workspace. Logger.log("d", "Workspace loading is checking stacks containers...") # -- # load global stack file try: # Check if a stack by this ID already exists; container_stacks = self._container_registry.findContainerStacks(id = global_stack_id_original) if container_stacks: stack = container_stacks[0] if self._resolve_strategies["machine"] == "override": # TODO: HACK # There is a machine, check if it has authentication data. If so, keep that data. network_authentication_id = container_stacks[0].getMetaDataEntry("network_authentication_id") network_authentication_key = container_stacks[0].getMetaDataEntry("network_authentication_key") container_stacks[0].deserialize(archive.open(global_stack_file).read().decode("utf-8")) if network_authentication_id: container_stacks[0].addMetaDataEntry("network_authentication_id", network_authentication_id) if network_authentication_key: container_stacks[0].addMetaDataEntry("network_authentication_key", network_authentication_key) elif self._resolve_strategies["machine"] == "new": stack = GlobalStack(global_stack_id_new) stack.deserialize(archive.open(global_stack_file).read().decode("utf-8")) # Ensure a unique ID and name stack._id = global_stack_id_new # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the # bound machine also needs to change. if stack.getMetaDataEntry("machine", None): stack.setMetaDataEntry("machine", global_stack_id_new) # Only machines need a new name, stacks may be non-unique stack.setName(self._container_registry.uniqueName(stack.getName())) container_stacks_added.append(stack) self._container_registry.addContainer(stack) else: Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) else: # no existing container stack, so we create a new one stack = GlobalStack(global_stack_id_new) # Deserialize stack by converting read data from bytes to string stack.deserialize(archive.open(global_stack_file).read().decode("utf-8")) container_stacks_added.append(stack) self._container_registry.addContainer(stack) containers_added.append(stack) global_stack = stack Job.yieldThread() except: Logger.logException("w", "We failed to serialize the stack. Trying to clean up.") # Something went really wrong. Try to remove any data that we added. for container in containers_added: self._container_registry.removeContainer(container.getId()) return # -- # load extruder stack files try: for index, extruder_stack_file in enumerate(extruder_stack_files): container_id = self._stripFileToId(extruder_stack_file) extruder_file_content = archive.open(extruder_stack_file, "r").read().decode("utf-8") container_stacks = self._container_registry.findContainerStacks(id = container_id) if container_stacks: # this container stack already exists, try to resolve stack = container_stacks[0] if self._resolve_strategies["machine"] == "override": # NOTE: This is the same code as those in the lower part # deserialize new extruder stack over the current ones stack = self._overrideExtruderStack(global_stack, extruder_file_content) elif self._resolve_strategies["machine"] == "new": # create a new extruder stack from this one new_id = extruder_stack_id_map[container_id] stack = ExtruderStack(new_id) # HACK: the global stack can have a new name, so we need to make sure that this extruder stack # references to the new name instead of the old one. Normally, this can be done after # deserialize() by setting the metadata, but in the case of ExtruderStack, deserialize() # also does addExtruder() to its machine stack, so we have to make sure that it's pointing # to the right machine BEFORE deserialization. extruder_config = configparser.ConfigParser() extruder_config.read_string(extruder_file_content) extruder_config.set("metadata", "machine", global_stack_id_new) tmp_string_io = io.StringIO() extruder_config.write(tmp_string_io) extruder_file_content = tmp_string_io.getvalue() stack.deserialize(extruder_file_content) # Ensure a unique ID and name stack._id = new_id self._container_registry.addContainer(stack) extruder_stacks_added.append(stack) containers_added.append(stack) else: # No extruder stack with the same ID can be found if self._resolve_strategies["machine"] == "override": # deserialize new extruder stack over the current ones stack = self._overrideExtruderStack(global_stack, extruder_file_content) elif self._resolve_strategies["machine"] == "new": # container not found, create a new one stack = ExtruderStack(container_id) # HACK: the global stack can have a new name, so we need to make sure that this extruder stack # references to the new name instead of the old one. Normally, this can be done after # deserialize() by setting the metadata, but in the case of ExtruderStack, deserialize() # also does addExtruder() to its machine stack, so we have to make sure that it's pointing # to the right machine BEFORE deserialization. extruder_config = configparser.ConfigParser() extruder_config.read_string(extruder_file_content) extruder_config.set("metadata", "machine", global_stack_id_new) tmp_string_io = io.StringIO() extruder_config.write(tmp_string_io) extruder_file_content = tmp_string_io.getvalue() stack.deserialize(extruder_file_content) self._container_registry.addContainer(stack) extruder_stacks_added.append(stack) containers_added.append(stack) else: Logger.log("w", "Unknown resolve strategy: %s" % str(self._resolve_strategies["machine"])) extruder_stacks.append(stack) except: Logger.logException("w", "We failed to serialize the stack. Trying to clean up.") # Something went really wrong. Try to remove any data that we added. for container in containers_added: self._container_registry.removeContainer(container.getId()) return # # Replacing the old containers if resolve is "new". # When resolve is "new", some containers will get renamed, so all the other containers that reference to those # MUST get updated too. # if self._resolve_strategies["machine"] == "new": # A new machine was made, but it was serialized with the wrong user container. Fix that now. for container in user_instance_containers: # replacing the container ID for user instance containers for the extruders extruder_id = container.getMetaDataEntry("extruder", None) if extruder_id: for extruder in extruder_stacks: if extruder.getId() == extruder_id: extruder.userChanges = container continue # replacing the container ID for user instance containers for the machine machine_id = container.getMetaDataEntry("machine", None) if machine_id: if global_stack.getId() == machine_id: global_stack.userChanges = container continue for changes_container_type in ("quality_changes", "definition_changes"): if self._resolve_strategies[changes_container_type] == "new": # Quality changes needs to get a new ID, added to registry and to the right stacks for each_changes_container in quality_and_definition_changes_instance_containers: # NOTE: The renaming and giving new IDs are possibly redundant because they are done in the # instance container loading part. new_id = each_changes_container.getId() # Find the old (current) changes container in the global stack if changes_container_type == "quality_changes": old_container = global_stack.qualityChanges elif changes_container_type == "definition_changes": old_container = global_stack.definitionChanges # sanity checks # NOTE: The following cases SHOULD NOT happen!!!! if not old_container: Logger.log("e", "We try to get [%s] from the global stack [%s] but we got None instead!", changes_container_type, global_stack.getId()) # Replace the quality/definition changes container if it's in the GlobalStack # NOTE: we can get an empty container here, but the IDs will not match, # so this comparison is fine. if self._id_mapping.get(old_container.getId()) == new_id: if changes_container_type == "quality_changes": global_stack.qualityChanges = each_changes_container elif changes_container_type == "definition_changes": global_stack.definitionChanges = each_changes_container continue # Replace the quality/definition changes container if it's in one of the ExtruderStacks for each_extruder_stack in extruder_stacks: changes_container = None if changes_container_type == "quality_changes": changes_container = each_extruder_stack.qualityChanges elif changes_container_type == "definition_changes": changes_container = each_extruder_stack.definitionChanges # sanity checks # NOTE: The following cases SHOULD NOT happen!!!! if not changes_container: Logger.log("e", "We try to get [%s] from the extruder stack [%s] but we got None instead!", changes_container_type, each_extruder_stack.getId()) # NOTE: we can get an empty container here, but the IDs will not match, # so this comparison is fine. if self._id_mapping.get(changes_container.getId()) == new_id: if changes_container_type == "quality_changes": each_extruder_stack.qualityChanges = each_changes_container elif changes_container_type == "definition_changes": each_extruder_stack.definitionChanges = each_changes_container if self._resolve_strategies["material"] == "new": for each_material in material_containers: old_material = global_stack.material # check if the old material container has been renamed to this material container ID # if the container hasn't been renamed, we do nothing. new_id = self._id_mapping.get(old_material.getId()) if new_id is None or new_id != each_material.getId(): continue if old_material.getId() in self._id_mapping: global_stack.material = each_material for each_extruder_stack in extruder_stacks: old_material = each_extruder_stack.material # check if the old material container has been renamed to this material container ID # if the container hasn't been renamed, we do nothing. new_id = self._id_mapping.get(old_material.getId()) if new_id is None or new_id != each_material.getId(): continue if old_material.getId() in self._id_mapping: each_extruder_stack.material = each_material if extruder_stacks: for stack in extruder_stacks: ExtruderManager.getInstance().registerExtruder(stack, global_stack.getId()) else: # Machine has no extruders, but it needs to be registered with the extruder manager. ExtruderManager.getInstance().registerExtruder(None, global_stack.getId()) Logger.log("d", "Workspace loading is notifying rest of the code of changes...") if self._resolve_strategies["machine"] == "new": for stack in extruder_stacks: stack.setNextStack(global_stack) stack.containersChanged.emit(stack.getTop()) # Actually change the active machine. Application.getInstance().setGlobalContainerStack(global_stack) # Notify everything/one that is to notify about changes. global_stack.containersChanged.emit(global_stack.getTop()) # Load all the nodes / meshdata of the workspace nodes = self._3mf_mesh_reader.read(file_name) if nodes is None: nodes = [] return nodes
def _setOutputDeviceMetadata(self, device: CloudOutputDevice, machine: GlobalStack): machine.setName(device.name) machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) machine.setMetaDataEntry("group_name", device.name) machine.addConfiguredConnectionType(device.connectionType.value)
def _setOutputDeviceMetadata(self, device: CloudOutputDevice, machine: GlobalStack): machine.setName(device.name) machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) machine.setMetaDataEntry(self.META_HOST_GUID, device.clusterData.host_guid) machine.setMetaDataEntry("group_name", device.name) machine.setMetaDataEntry("group_size", device.clusterSize) digital_factory_string = self.i18n_catalog.i18nc( "info:name", "Ultimaker Digital Factory") digital_factory_link = "<a href='https://digitalfactory.ultimaker.com?utm_source=cura&utm_medium=software&utm_campaign=change-account-remove-printer'>{digital_factory_string}</a>".format( digital_factory_string=digital_factory_string) removal_warning_string = self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "{printer_name} will be removed until the next account sync.").format(printer_name = device.name) \ + "<br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "To remove {printer_name} permanently, visit {digital_factory_link}").format(printer_name = device.name, digital_factory_link = digital_factory_link) \ + "<br><br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "Are you sure you want to remove {printer_name} temporarily?").format(printer_name = device.name) machine.setMetaDataEntry("removal_warning", removal_warning_string) machine.addConfiguredConnectionType(device.connectionType.value)