class DSSWikiConfluenceExporter(Runnable, WikiTransfer): def __init__(self, project_key, config, plugin_config): """ :param project_key: the project in which the runnable executes :param config: the dict of the configuration of the object :param plugin_config: contains the plugin settings """ self.project_key = project_key self.config = config confluence_login = self.config.get("confluence_login", None) if confluence_login is None: raise Exception("No Confluence login is currently set.") self.confluence_username = confluence_login.get( "confluence_username", None) self.assert_confluence_username() self.confluence_password = confluence_login.get( "confluence_password", None) self.assert_confluence_password() self.confluence_url = self.format_confluence_url( confluence_login.get("server_type", None), confluence_login.get("url", None), confluence_login.get("orgname", None)) self.confluence_space_key = confluence_login.get( "confluence_space_key", None) self.assert_space_key() self.confluence_space_name = confluence_login.get( "confluence_space_name", self.confluence_space_key) if self.confluence_space_name == "": self.confluence_space_name = self.confluence_space_key self.check_space_key_format() self.client = dataiku.api_client() try: self.studio_external_url = self.client.get_general_settings( ).get_raw()['studioExternalUrl'] assert (self.studio_external_url not in (None, '')) except Exception as err: logger.error("studioExternalUrl not set :{}".format(err)) raise Exception( "Please set the DSS location URL in Administration > Settings > Notifications & Integrations > DSS Location > DSS URL" ) self.wiki = DSSWiki(self.client, self.project_key) self.wiki_settings = self.wiki.get_settings() self.taxonomy = self.wiki_settings.get_taxonomy() self.articles = self.wiki.list_articles() self.space_homepage_id = None self.confluence = Confluence(url=self.confluence_url, username=self.confluence_username, password=self.confluence_password) self.assert_logged_in() self.progress = 0 def get_progress_target(self): return (len(self.articles), 'FILES') def run(self, progress_callback): self.progress_callback = progress_callback space = self.confluence.get_space(self.confluence_space_key) if space is None: raise Exception( 'Empty answer from server. Please check the Confluence server address.' ) if "id" not in space: space = self.confluence.create_space(self.confluence_space_key, self.confluence_space_name) if space is None: space = self.confluence.get_space(self.confluence_space_key) if u'statusCode' in space and space[u'statusCode'] == 404: raise Exception( 'Could not create the "' + self.confluence_space_key + '" space. It probably exists but you don\'t have permission to view it, or the casing is wrong.' ) if space is not None and "homepage" in space: self.space_homepage_id = space['homepage']['id'] else: self.space_homepage_id = None self.recurse_taxonomy(self.taxonomy, self.space_homepage_id) if self.space_homepage_id is not None: self.update_landing_page(self.space_homepage_id) return self.confluence_url + "/display/" + self.confluence_space_key def assert_logged_in(self): try: user_details = self.confluence.get_user_details_by_userkey( self.confluence_username) except Exception as err: logger.error("get_user_details_by_userkey failed:{}".format(err)) raise Exception( 'Could not connect to Confluence server. Please check the connection details' ) if user_details is None: raise Exception( 'No answer from the server. Please check the connection details to the Confluence server.' ) if "HTTP Status 401 – Unauthorized" in user_details: raise Exception( 'No valid Confluence credentials, please check login and password' ) if "errorMessage" in user_details: raise Exception( 'Error while accessing Confluence site : {}'.format( user_details["errorMessage"])) if "status-code" in user_details and user_details["status-code"] > 400: if "message" in user_details: raise Exception( 'Error while accessing Confluence site : {}'.format( user_details["message"])) else: raise Exception( 'Error {} while accessing Confluence site'.format( user_details["status-code"])) def assert_space_key(self): space_name_format = re.compile(r'^[a-zA-Z0-9]+$') if self.confluence_space_key is None or space_name_format.match( self.confluence_space_key) is None: raise Exception( 'The space key does not match Confluence requirements ([a-z], [A-Z], [0-9], not space)' ) def assert_confluence_username(self): username_format = re.compile(r'^[a-z0-9-.@]+$') if self.confluence_username is None or username_format.match( self.confluence_username) is None: raise Exception('The Confluence user name is not valid') def assert_confluence_password(self): if self.confluence_password is None or self.confluence_password == "": raise Exception('Please set your Confluence login password')
class ConfluenceAdapter(object): """ Adapter for Atlassian Confluence class. Encapsulates content retrieve and update functionality. """ NAMESPACES = { "atlassian-content": "http://atlassian.com/content", "ac": "http://atlassian.com/content", "ri": "http://atlassian.com/content", "atlassian-template": "http://atlassian.com/template", "at": "http://atlassian.com/template", } def __init__(self, url, username, password, space_key): self.confluence = Confluence(url=url, username=username, password=password) self.space_key = space_key self.get_space_or_create() def get_space_or_create(self): """ Check whether space exists or not. If it doesn't, then create the space. :rtype: dict :return: Space data. """ space = self.confluence.get_space(self.space_key) if type(space) is not dict: raise WikiUpdateException("Can't retrieve valid information about Confluence space." " Please check configurations. Data: {}".format(space)) if space.get('statusCode', None) == 404: space = self.confluence.create_space(self.space_key, self.space_key) return space def get_page_or_create(self, page_title): """ Get page content, if no such page then create it. :type page_title: str :param page_title: Title of the page which should be retrieved. :raises: WikiUpdateException :rtype: tuple(int, lxml.etree._Element) :returns: Tuple where first element is the id of the page. The second is the parsed content data. """ data = self.confluence.get_page_by_title(title=page_title, space=self.space_key, expand="body.storage,version") if not data: # No such page exist. Then create such page. data = self.create_page(page_title) try: content_xml = data['body']['storage']['value'] except KeyError: raise WikiUpdateException("Can't get partial-devices page content.") body_xml = render_to_string('wrapper.xml', { 'content': content_xml }) return data['id'], etree.fromstring(body_xml) def create_page(self, page_title): """ Create new page.+ :type page_title: str :param page_title: Title of the page which should be created. :raises: WikiUpdateException :rtype: dict :return: Data of newly created page. """ data = self.confluence.create_page(self.space_key, page_title, body="") if not data or 'id' not in data: raise WikiUpdateException("Page `{}` could not be created. Response data: {}".format(page_title, data)) return data def update_page_content(self, page_id, page_title, body): """ Update existing page with new body. :type page_id: int :param page_id: Page id which should be modified. :type page_title: str :param page_title: Title of the page which should be modified. :type body: lxml.etree._Element :param body: Page content data. :raises: WikiUpdateException :rtype: dict :returns: Data of newly updated page. """ # Get raw xml. body_xml = etree.tostring(body).decode('utf-8') # <xml xmlns:******> <p></p>***<ac:structured-macro>***</ac:structured-macro> </xml> # ^ ^ # Take the content starting right after `>` of the opening xml tag and till `<` of xml closing tag. content_xml = body_xml[body_xml.find('>') + 1:body_xml.rfind('<')] data = self.confluence.update_existing_page(page_id, page_title, content_xml) if not data or 'id' not in data: raise WikiUpdateException("Page `{}` could not be updated. Response data: {}".format(page_title, data)) return data @classmethod def get_field_element(cls, element, field): """ Get element from tree by field name. :rtype: lxml.etree._Element :returns: Element data. """ return element.xpath('//ac:structured-macro/ac:parameter' '[@ac:name="MultiExcerptName" and translate(normalize-space(text()), " ", "")="{}"]' '/following-sibling::ac:rich-text-body/*[local-name()="p"]'.format(field.name), namespaces=cls.NAMESPACES) @classmethod def update_content_for_field(cls, page_content, field): """ Update content for field. :type page_content: lxml.etree._Element :param page_content: Wiki page content. :type field: AbstractLinkedField :param field: Field for which the page_content should be updated. :rtype: lxml.etree._Element :returns: Page content data. """ field_elements = cls.get_field_element(page_content, field) if not field_elements: # If element does not exist then create it. macro_id = uuid.uuid4() data = render_to_string('multiexcerpt.xml', { "macro_id": macro_id, "field_name": field.name, "field_value": field.provide_value() }) element = etree.fromstring(data) page_content.insert(-1, element) # Can leave without this return, but `Explicit is better than implicit.` (C) Python Zen. return page_content for field_element in field_elements: field_element.text = field.provide_value() return page_content