# -*- coding: utf-8 -*-
"""
:Module: salespyforce.knowledge
:Synopsis: Defines the Knowledge-related functions associated with the Salesforce API
:Created By: Jeff Shurtliff
:Last Modified: Jeff Shurtliff
:Modified Date: 28 Feb 2026
"""
from __future__ import annotations
from typing import Optional, Union, Tuple
from . import errors
from . import constants as const
from .utils import log_utils
from .utils.core_utils import ensure_ends_with
# Initialize logging
logger = log_utils.initialize_logging(__name__)
[docs]
def check_for_existing_article(
sfdc_object,
title: str,
sobject: Optional[str] = None,
return_id: bool = False,
return_id_and_number: bool = False,
include_archived: bool = False,
) -> Union[str, Tuple[str, str]]:
"""This method checks to see if an article already exists with a given title and returns its article number.
(`Reference 1 <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_query.htm>`__,
`Reference 2 <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/knowledge_development_soql_sosl_intro.htm>`__)
.. versionchanged:: 1.2.2
You can now specify whether archived articles are included in the query results.
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param title: The title of the knowledge article for which to check
:type title: str
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
:type sobject: str, None
:param return_id: Determines if the Article ID should be returned (``False`` by default)
:type return_id: bool
:param return_id_and_number: Determines if Article ID and Article Number should be returned (``False`` by default)
:type return_id_and_number: bool
:param include_archived: Determines if archived articles should be included (``False`` by default)
:type include_archived: bool
:returns: The Article Number, Article ID, or both, if found, or a blank string if not found
:raises: :py:exc:`TypeError`
"""
# Prepare the SOQL query
sobject = _validate_knowledge_sobject(sobject)
query = f"""
SELECT {const.SOBJECT_FIELDS.ID}, {const.SOBJECT_FIELDS.ARTICLE_NUMBER}
FROM {sobject}
WHERE {const.SOBJECT_FIELDS.TITLE} = '{title}'
"""
if not include_archived:
query += f" AND {const.SOBJECT_FIELDS.PUBLISH_STATUS} != '{const.SOBJECT_FIELD_VALUES.ARCHIVED}'"
# Perform and parse the SOQL query
response = sfdc_object.soql_query(query, replace_quotes=False)
if response.get(const.RESPONSE_KEYS.TOTAL_SIZE) > 0:
if return_id:
return_value = response[const.RESPONSE_KEYS.RECORDS][0][const.SOBJECT_FIELDS.ID]
elif return_id_and_number:
return_value = (
response[const.RESPONSE_KEYS.RECORDS][0][const.SOBJECT_FIELDS.ID],
response[const.RESPONSE_KEYS.RECORDS][0][const.SOBJECT_FIELDS.ARTICLE_NUMBER],
)
else:
return_value = response[const.RESPONSE_KEYS.RECORDS][0][const.SOBJECT_FIELDS.ARTICLE_NUMBER]
elif return_id_and_number:
return_value = ('', '')
else:
return_value = ''
return return_value
[docs]
def get_article_id_from_number(
sfdc_object,
article_number: Union[str, int],
sobject: Optional[str] = None,
return_uri: bool = False,
) -> str:
"""This method returns the Article ID when an article number is provided.
(`Reference 1 <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_query.htm>`__,
`Reference 2 <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/knowledge_development_soql_sosl_intro.htm>`__)
.. warning::
The ability to retrieve the article URI/URL rather than the ID will be moved to a separate function in
a future release.
.. versionchanged:: 1.4.0
A logic issue has been fixed and improved to make this function more robust and stable.
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_number: The Article Number to query
:type article_number: str, int
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
:type sobject: str, None
:param return_uri: Determines if the URI of the article should be returned rather than the ID (``False`` by default)
:type return_uri: bool
:returns: The Article ID or Article URI, or a blank string if no article is found
:raises: :py:exc:`TypeError`,
:py:exc:`RuntimeError`
"""
# Ensure the sobject is defined appropriately
sobject = _validate_knowledge_sobject(sobject)
# Construct the SOQL query to perform
if not isinstance(article_number, str):
article_number = str(article_number)
query = f'SELECT {const.SOBJECT_FIELDS.ID} FROM {sobject} '
if len(article_number) < 9:
query += f"WHERE {const.SOBJECT_FIELDS.ARTICLE_NUMBER} LIKE '%0{article_number}'"
else:
query += f"WHERE {const.SOBJECT_FIELDS.ARTICLE_NUMBER} = '{article_number}'"
# Perform the SOQL query and return the article number if found
response = sfdc_object.soql_query(query)
if response.get(const.RESPONSE_KEYS.TOTAL_SIZE) > 0:
if return_uri:
# TODO: Split out the return_uri functionality into a separate function and method
warn_msg = ("The ability to retrieve the article URI/URL rather than the ID (return_uri parameter) will "
"be moved to a separate function/method in a future release")
logger.warning(warn_msg)
errors.handlers.display_warning(warn_msg)
return_value = response[const.RESPONSE_KEYS.RECORDS][0][const.RESPONSE_KEYS.ATTRIBUTES][const.RESPONSE_KEYS.URL]
else:
return_value = response[const.RESPONSE_KEYS.RECORDS][0][const.SOBJECT_FIELDS.ID]
else:
return_value = ''
warn_msg = f'No results were returned when querying for the article number {article_number}'
logger.warning(warn_msg)
return return_value
[docs]
def get_articles_list(
sfdc_object,
query: Optional[str] = None,
sort: Optional[str] = None,
order: Optional[str] = None,
page_size: int = const.QUERY_PARAMS.DEFAULT_PAGE_SIZE, # Default: 20
page_num: int = const.QUERY_PARAMS.DEFAULT_PAGE_NUM, # Default: 1
) -> list:
"""This function retrieves a list of knowledge articles.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_knowledge_support_artlist.htm>`__)
.. versionchanged:: 1.4.0
The errors now log as errors via the logger rather than to the stderr console.
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param query: A SOQL query with which to filter the results (optional)
:type query: str, None
:param sort: One of the following optional values: ``LastPublishedDate``, ``CreatedDate``, ``Title``, or ``ViewScore``
:type sort: str, None
:param order: Optionally define the ORDER BY as ``ASC`` or ``DESC``
:type order: str, None
:param page_size: The number of results per page (``20`` by default)
:type page_size: int
:param page_num: The starting page number (``1`` by default)
:type page_num: int
:returns: The list of retrieved knowledge articles
"""
# Define the headers
headers = sfdc_object._get_headers(const.HEADER_TYPE_ARTICLES)
# Validate the sort parameter and ignore the value if it is invalid
if sort and sort not in const.SOBJECT_FIELDS.VALID_KNOWLEDGE_SORT_FIELDS:
logger.error(const._LOG_MESSAGES._INVALID_PARAM_VALUE_IGNORE.format(
param=const.QUERY_PARAMS.SORT,
value=sort
))
sort = None
# Validate the order parameter and ignore the value if it is invalid
if order and order.upper() not in const.SOQL_QUERIES.VALID_ORDER_DIRECTIONS:
logger.error(const._LOG_MESSAGES._INVALID_PARAM_VALUE_IGNORE.format(
param=const.QUERY_PARAMS.ORDER,
value=order
))
order = None
# Validate the page size parameter (Fall back to maximum value rather than default value if maximum is exceeded)
if page_size > const.QUERY_PARAMS.MAX_PAGE_SIZE:
logger.error(const._LOG_MESSAGES._PARAM_EXCEEDS_MAX_VALUE.format(
param=const.QUERY_PARAMS.PAGE_SIZE,
default=const.QUERY_PARAMS.MAX_PAGE_SIZE
))
page_size = const.QUERY_PARAMS.MAX_PAGE_SIZE
# Validate the pageNumber parameter and fall back to default value if it is invalid
if page_num < const.QUERY_PARAMS.MIN_PAGE_NUM:
logger.error(const._LOG_MESSAGES._INVALID_PARAM_VALUE_DEFAULT.format(
param=const.QUERY_PARAMS.PAGE_NUM,
default=const.QUERY_PARAMS.DEFAULT_PAGE_NUM
))
page_num = const.QUERY_PARAMS.DEFAULT_PAGE_NUM
# Add values to the parameters dictionary if they have been defined
params = {}
if query:
params[const.QUERY_PARAMS.Q] = query
if sort:
params[const.QUERY_PARAMS.SORT] = sort
if order:
params[const.QUERY_PARAMS.ORDER] = order
params[const.QUERY_PARAMS.PAGE_SIZE] = page_size
params[const.QUERY_PARAMS.PAGE_NUM] = page_num
# Perform the query
# TODO: Determine what is returned by this API call and see if data should be pruned to just the list of articles
endpoint = const.REST_PATHS.KNOWLEDGE_ARTICLES.format(api_version=sfdc_object.version)
return sfdc_object.get(endpoint, params=params, headers=headers)
[docs]
def get_article_details(
sfdc_object,
article_id: str,
sobject: Optional[str] = None,
use_knowledge_articles_endpoint: Optional[bool] = None,
):
"""This function retrieves details for a single knowledge article.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_knowledge_support_artdetails.htm>`__)
.. versionchanged:: 1.4.0
A logic issue was resolved and the new optional ``use_knowledge_articles_endpoint`` parameter can now be set to
force the ``knowledgeArticles`` endpoint to be used for the GET request rather than the ``sobjects`` endpoint.
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_id: The Article ID for which to retrieve details
:type article_id: str
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
:type sobject: str, None
:param use_knowledge_articles_endpoint: Optionally use the ``knowledgeArticles`` endpoint rather than ``sobjects``
to retrieve the article details (``False`` by default)
:type use_knowledge_articles_endpoint: bool, None
:returns: The details for the knowledge article
:raises: :py:exc:`RuntimeError`,
:py:exc:`salespyforce.errors.exceptions.DataMismatchError`
"""
# Define the headers based on the endpoint that will be utilized
headers = sfdc_object._get_headers(const.HEADER_TYPE_ARTICLES) if use_knowledge_articles_endpoint else None
# Ensure the sobject is defined appropriately
sobject = _validate_knowledge_sobject(sobject, use_knowledge_articles_endpoint)
# Define the endpoint to use in the GET request
if use_knowledge_articles_endpoint:
endpoint = const.REST_PATHS.KNOWLEDGE_ARTICLES_BY_ID.format(
api_version=sfdc_object.version,
article_id=article_id,
)
else:
endpoint = const.REST_PATHS.SOBJECT_BY_ID.format(
api_version=sfdc_object.version,
sobject=sobject,
record_id=article_id,
)
# Perform the query and return the data
data = sfdc_object.get(endpoint, headers=headers)
# TODO: Determine what is returned by this API call and see if data should be pruned to just the article details (for both endpoints)
return data
[docs]
def get_validation_status(
sfdc_object,
article_id: Optional[str] = None,
article_details: Optional[dict] = None,
sobject: Optional[str] = None,
use_knowledge_articles_endpoint: Optional[bool] = None,
) -> str:
"""This function retrieves the Validation Status for a given Article ID.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_knowledge_support_artdetails.htm>`__)
.. versionchanged:: 1.5.0
The `use_knowledge_articles_endpoint` parameter is now supported, which allows you to specify the
REST path to utilize for the API query.
.. versionchanged:: 1.4.0
The function now returns an empty string rather than a ``None`` value if the ``ValidationStatus`` field
is not found in the article details data, and a more specific exception class is used when input
data is missing instead of the generic :py:exc:`RuntimeError` exception class.
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_id: The Article ID for which to retrieve details
:type article_id: str, None
:param article_details: The dictionary of article details for the given article
:type article_details: dict, None
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
:type sobject: str, None
:param use_knowledge_articles_endpoint: Optionally use the ``knowledgeArticles`` endpoint rather than ``sobjects``
to retrieve the article details (``False`` by default)
:type use_knowledge_articles_endpoint: bool, None
:returns: The validation status as a text string
:raises: :py:exc:`RuntimeError`,
:py:exc:`salespyforce.errors.exceptions.MissingRequiredDataError`
"""
if not any((article_id, article_details)):
error_msg = const._LOG_MESSAGES._MUST_BE_PROVIDED_ERROR.format(data='article ID or article details')
logger.error(error_msg)
raise errors.exceptions.MissingRequiredDataError(error_msg)
# Retrieve the article details if not already supplied
if not article_details:
article_details = get_article_details(sfdc_object, article_id, sobject, use_knowledge_articles_endpoint)
# Identify the validation status
return article_details.get(const.SOBJECT_FIELDS.VALIDATION_STATUS, '')
[docs]
def get_article_metadata(sfdc_object, article_id: str):
"""This function retrieves metadata for a specific knowledge article.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/knowledge_REST_retrieve_article_metadata.htm>`__)
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_id: The Article ID for which to retrieve details
:type article_id: str
:returns: The article metadata as a dictionary
:raises: :py:exc:`RuntimeError`
"""
# TODO: Update :raises: with correct exceptions
endpoint = const.REST_PATHS.KNOWLEDGE_ARTICLES_BY_ID.format(
api_version=sfdc_object.version,
article_id=article_id,
)
return sfdc_object.get(endpoint)
[docs]
def get_article_version(sfdc_object, article_id: str):
"""This function retrieves the version ID for a given master article ID.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/knowledge_REST_retrieve_article_version.htm>`__)
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_id: The Article ID for which to retrieve details
:type article_id: str
:returns: The version ID for the given master article ID
:raises: :py:exc:`RuntimeError`
"""
# TODO: Update :raises: with correct exceptions
endpoint = const.REST_PATHS.ARTICLE_MASTER_VERSION_BY_ID.format(
api_version=sfdc_object.version,
article_id=article_id,
)
# TODO: Determine what is returned by this API call and see if data should be pruned to just the Version ID
return sfdc_object.get(endpoint)
[docs]
def get_article_url(
sfdc_object,
article_id: Optional[str] = None,
article_number: Union[Optional[str], Optional[int]] = None,
sobject: Optional[str] = None,
) -> str:
"""This function constructs the URL to view a knowledge article in Lightning or Classic.
.. versionchanged:: 1.2.0
Changed when lightning URLs are defined and fixed an issue with extraneous slashes.
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_id: The Article ID for which to retrieve details
:type article_id: str, None
:param article_number: The article number for which to retrieve details
:type article_number: str, int, None
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
:type sobject: str, None
:returns: The article URL as a string
:raises: :py:exc:`salespyforce.errors.exceptions.MissingRequiredDataError`
"""
sobject = _validate_knowledge_sobject(sobject)
if not any((article_id, article_number)):
exc_msg = 'An article ID or an article number must be provided to retrieve the article URL.'
raise errors.exceptions.MissingRequiredDataError(exc_msg)
if article_number and not article_id:
article_id = get_article_id_from_number(sfdc_object, article_number, sobject)
if 'lightning' in sfdc_object.base_url or sobject == const.SOBJECTS.KNOWLEDGE:
article_url = const.URLS.LIGHTNING_RECORD_PAGE.format(
base_url=ensure_ends_with(sfdc_object.base_url, '/'),
sobject=sobject,
record_id=article_id,
)
else:
article_url = const.URLS.CLASSIC_ARTICLE_DRAFT.format(
base_url=ensure_ends_with(sfdc_object.base_url, '/'),
article_id=article_id,
)
return article_url
[docs]
def create_article(
sfdc_object,
article_data: dict,
sobject: Optional[str] = None,
full_response: bool = False,
):
"""This function creates a new knowledge article draft.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_create.htm>`__)
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_data: The article data used to populate the article
:type article_data: dict
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
:type sobject: str, None
:param full_response: Determines if the full API response should be returned instead of the article ID (``False`` by default)
:type full_response: bool
:returns: The API response or the ID of the article draft
:raises: :py:exc:`ValueError`,
:py:exc:`TypeError`,
:py:exc:`RuntimeError`
"""
# TODO: Update :raises: with correct exceptions
# Ensure the sobject is defined appropriately
sobject = _validate_knowledge_sobject(sobject)
# Ensure the payload is in the appropriate format
_validate_article_data(article_data)
# Ensure that the required fields have been provided
_check_required_article_fields(article_data)
# Define the endpoint and perform the API call
endpoint = const.REST_PATHS.SOBJECT.format(
api_version=sfdc_object.version,
sobject=sobject,
)
response = sfdc_object.post(endpoint, payload=article_data)
# Return the full response or just the article ID
if not full_response:
# TODO: Verify that the `id` value below is correct and shouldn't be `Id` instead
response = response.get('id')
return response
[docs]
def update_article(
sfdc_object,
record_id: str,
article_data: dict,
sobject: Optional[str] = None,
include_status_code: bool = False,
):
"""This function updates an existing knowledge article draft.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_update_fields.htm>`__)
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param record_id: The ID of the article draft record to be updated
:type record_id: str
:param article_data: The article data used to update the article
:type article_data: dict
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
:type sobject: str, None
:param include_status_code: Determines if the API response status code should be returned (``False`` by default)
:type include_status_code: bool
:returns: A Boolean indicating if the update operation was successful, and optionally the API response status code
:raises: :py:exc:`ValueError`,
:py:exc:`TypeError`,
:py:exc:`RuntimeError`
"""
# TODO: Update :raises: with correct exceptions
# Ensure the sobject is defined appropriately
sobject = _validate_knowledge_sobject(sobject)
# Ensure the payload is in the appropriate format
_validate_article_data(article_data)
# Ensure that the required fields have been provided
_check_required_article_fields(article_data)
# Define the endpoint and perform the API call
endpoint = const.REST_PATHS.SOBJECT_BY_ID.format(
api_version=sfdc_object.version,
sobject=sobject,
record_id=record_id,
)
response = sfdc_object.patch(endpoint, payload=article_data)
# Determine whether the call was successful
successful = True if response.status_code == 204 else False
# Return the success determination and optionally the status code
if include_status_code:
return successful, response.status_code
return successful
[docs]
def create_draft_from_online_article(sfdc_object, article_id: str, unpublish: bool = False):
"""This function creates a draft knowledge article from an online article.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/actions_obj_knowledge.htm#createDraftFromOnlineKnowledgeArticle>`__)
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_id: The ID of the online article from which to create the draft
:type article_id: str
:param unpublish: Determines if the online article should be unpublished when the draft is created (``False`` by default)
:type unpublish: bool
:returns: The API response from the POST request
:raises: :py:exc:`RuntimeError`
"""
# TODO: Update :raises: with correct exceptions
# Define the payload for the API call
payload = {
const.QUERY_PARAMS.INPUTS: [
{
const.QUERY_PARAMS.ACTION: const.PAYLOAD_VALUES.EDIT_AS_DRAFT,
const.QUERY_PARAMS.UNPUBLISH: unpublish,
const.QUERY_PARAMS.ARTICLE_ID: article_id,
}
]
}
# Define the endpoint and perform the API call
endpoint = const.REST_PATHS.CREATE_DRAFT_FROM_ONLINE_ARTICLE.format(
api_version=sfdc_object.version
)
return sfdc_object.post(endpoint, payload)
[docs]
def create_draft_from_master_version(
sfdc_object,
article_id: Optional[str] = None,
knowledge_article_id: Optional[str] = None,
article_data: Optional[dict] = None,
sobject: Optional[str] = None,
full_response: bool = False,
):
"""This function creates an online version of a master article.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.198.0.knowledge_dev.meta/knowledge_dev/knowledge_REST_edit_online_master.htm>`__)
.. versionchanged:: 1.5.0
The :py:exc:`salespyforce.errors.exceptions.MissingRequiredDataError` exception class is now raised when
required parameters are missing instead of the generic :py:exc:`RuntimeError` exception.
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_id: The Article ID from which to create the draft
:type article_id: str, None
:param knowledge_article_id: The Knowledge Article ID (``KnowledgeArticleId``) from which to create the draft
:type knowledge_article_id: str, None
:param article_data: The article data associated with the article from which to create the draft
:type article_data: dict, None
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
:type sobject: str, None
:param full_response: Determines if the full API response should be returned instead of the article ID (``False`` by default)
:type full_response: bool
:returns: The API response or the ID of the article draft
:raises: :py:exc:`salespyforce.errors.exceptions.MissingRequiredDataError`
"""
# TODO: Update :raises: with correct exceptions
if not any((article_id, knowledge_article_id, article_data)):
error_msg = 'Need to provide article ID, knowledge article ID, or article data'
logger.error(error_msg)
raise errors.exceptions.MissingRequiredDataError(error_msg)
# Ensure the sobject is defined appropriately
sobject = _validate_knowledge_sobject(sobject)
# Ensure the payload is in the appropriate format
_validate_article_data(article_data)
# Get the knowledge article ID as needed
if not knowledge_article_id:
if not article_data:
article_data = sfdc_object.get_article_details(article_id, sobject=sobject)
knowledge_article_id = article_data.get(const.SOBJECT_FIELDS.KNOWLEDGE_ARTICLE_ID)
# Perform the API call to retrieve the new draft ID
endpoint = const.REST_PATHS.KNOWLEDGE_MANAGEMENT_MASTER_VERSIONS.format(api_version=sfdc_object.version)
response = sfdc_object.post(endpoint, {const.QUERY_PARAMS.ARTICLE_ID: knowledge_article_id})
# Return the full response or the draft ID
if not full_response:
# TODO: Verify that the `id` value below is correct and shouldn't be `Id` instead
response = response.get('id')
return response
[docs]
def publish_article(
sfdc_object,
article_id: str,
major_version: bool = True,
full_response: bool = False,
):
"""This function publishes a draft knowledge article as a major or minor version.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/knowledge_REST_publish_master_version.htm>`__)
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_id: The Article ID to publish
:type article_id: str
:param major_version: Determines if the published article should be a major version (``True`` by default)
:type major_version: bool
:param full_response: Determines if the full API response should be returned (``False`` by default)
:type full_response: bool
:returns: A Boolean value indicating the success of the action or the API response from the PATCH request
:raises: :py:exc:`RuntimeError`
"""
# TODO: Update :raises: with correct exceptions
# Define the payload for the API call
payload = {
const.QUERY_PARAMS.PUBLISH_STATUS: const.PAYLOAD_VALUES.ONLINE
}
if major_version:
payload[const.QUERY_PARAMS.VERSION_NUMBER] = const.PAYLOAD_VALUES.NEXT_VERSION
# Perform the API call
# TODO: Replace the REST path below with a constant
endpoint = f'/services/data/{sfdc_object.version}/knowledgeManagement/articleVersions/masterVersions/{article_id}'
result = sfdc_object.patch(endpoint, payload)
# Return the appropriate value depending on if a full response was requested
if not full_response:
result = True if result.status_code == 204 else False
return result
[docs]
def publish_multiple_articles(sfdc_object, article_id_list: list, major_version: bool = True):
"""This function publishes multiple knowledge article drafts at one time.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/actions_obj_knowledge.htm#publishKnowledgeArticles>`__)
.. versionchanged:: 1.5.0
The :py:exc:`salespyforce.errors.exceptions.MissingRequiredDataError` exception class is now raised
when required parameters are missing instead of a more generic exception.
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_id_list: A list of Article IDs to be published
:type article_id_list: list
:param major_version: Determines if the published article should be a major version (``True`` by default)
:type major_version: bool
:returns: The API response from the POST request
:raises: :py:exc:`RuntimeError`,
:py:exc:`salespyforce.errors.exceptions.MissingRequiredDataError`
"""
# TODO: Update :raises: with correct exceptions
# Define the endpoint URI
endpoint = const.REST_PATHS.PUBLISH_KNOWLEDGE_ARTICLES.format(api_version=sfdc_object.version)
# Ensure there is at least one article ID to publish
validation_error = None
if not isinstance(article_id_list, list) or not isinstance(article_id_list[0], str):
validation_error = 'A list of Article ID strings must be provided in order to publish multiple articles.'
elif len(article_id_list) == 0:
validation_error = 'No article ID strings were found in the article ID list variable.'
if validation_error:
logger.error(validation_error)
raise errors.exceptions.MissingRequiredDataError(validation_error)
# Define the action to perform
action = const.PAYLOAD_VALUES.PUBLISH_ARTICLE_NEW_VERSION if major_version else const.PAYLOAD_VALUES.PUBLISH_ARTICLE
# Construct the payload
payload = {
const.QUERY_PARAMS.INPUTS: [
{
const.QUERY_PARAMS.ARTICLE_VERSION_ID_LIST: article_id_list,
const.QUERY_PARAMS.PUBLISH_ACTION: action
}
]
}
# Perform the API call
return sfdc_object.post(endpoint, payload)
[docs]
def assign_data_category(sfdc_object, article_id: str, category_group_name: str, category_name: str):
"""This function assigns a single data category for a knowledge article.
(`Reference <https://itsmemohit.medium.com/quick-win-15-salesforce-knowledge-rest-apis-bb0725b2040e>`__)
.. versionadded:: 1.2.0
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_id: The ID of the article to update
:type article_id: str
:param category_group_name: The unique Data Category Group Name
:type category_group_name: str
:param category_name: The unique Data Category Name
:type category_name: str
:returns: The API response from the POST request
:raises: :py:exc:`RuntimeError`
"""
# TODO: Update :raises: with correct exceptions
# Define the payload for the API call
payload = {
const.SOBJECT_FIELDS.PARENT_ID: article_id,
const.SOBJECT_FIELDS.DATA_CATEGORY_GROUP_NAME: category_group_name,
const.SOBJECT_FIELDS.DATA_CATEGORY_NAME: category_name
}
# Define the endpoint and perform the API call
endpoint = const.REST_PATHS.SOBJECT.format(
api_version=sfdc_object.version,
sobject=const.SOBJECTS.KNOWLEDGE_DATA_CATEGORY_SELECTION,
)
return sfdc_object.post(endpoint, payload)
[docs]
def archive_article(sfdc_object, article_id: str):
"""This function archives a published knowledge article.
(`Reference <https://developer.salesforce.com/docs/atlas.en-us.knowledge_dev.meta/knowledge_dev/knowledge_REST_archive_master_version.htm>`__)
.. versionadded:: 1.3.0
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param article_id: The ID of the article to archive
:type article_id: str
:returns: The API response from the PATCH request
:raises: :py:exc:`RuntimeError`
"""
# TODO: Update :raises: with correct exceptions
# Define the payload for the API call
payload = {
const.QUERY_PARAMS.PUBLISH_STATUS: const.PAYLOAD_VALUES.ARCHIVED
}
# Define the endpoint and perform the API call
endpoint = const.REST_PATHS.ARTICLE_MASTER_VERSION_BY_ID.format(
api_version=sfdc_object.version,
article_id=article_id,
)
return sfdc_object.patch(endpoint, payload)
[docs]
def delete_article_draft(sfdc_object, version_id: str, sobject: Optional[str] = None,
use_knowledge_management_endpoint: bool = True):
"""This function deletes an unpublished knowledge article draft.
.. versionchanged:: 1.5.0
An optional ``sobject`` parameter can now be passed to specify the sObject against which to query.
.. versionadded:: 1.4.0
:param sfdc_object: The instantiated SalesPyForce object
:type sfdc_object: class[salespyforce.Salesforce]
:param version_id: The 15-character or 18-character ``Id`` (Knowledge Article Version ID) value
:type version_id: str
:param sobject: The Salesforce object to query (``Knowledge__kav`` by default)
:type sobject: str, None
:param use_knowledge_management_endpoint: Leverage the ``/knowledgeManagement/articleVersions/masterVersions/``
endpoint rather than the ``/sobjects/Knowledge__kav/`` endpoint
(``True`` by default)
:type use_knowledge_management_endpoint: bool
:returns: The API response from the DELETE request
:raises: :py:exc:`RuntimeError`
"""
# TODO: Update :raises: with correct exceptions
# Ensure the sobject is defined appropriately
sobject = _validate_knowledge_sobject(sobject, use_knowledge_management_endpoint)
# Define the appropriate REST path and perform the API call
if use_knowledge_management_endpoint:
endpoint = const.REST_PATHS.ARTICLE_MASTER_VERSION_BY_ID.format(
api_version=sfdc_object.version,
article_id=version_id,
)
else:
endpoint = const.REST_PATHS.SOBJECT_BY_ID.format(
api_version=sfdc_object.version,
sobject=sobject,
record_id=version_id,
)
return sfdc_object.delete(endpoint)
def _validate_knowledge_sobject(
_sobject: Optional[str] = None,
_use_knowledge_articles_endpoint: Optional[bool] = None,
) -> str:
"""This function validates that a Knowledge sObject exists and supplies the default ``Knowledge__kav``
object when missing.
.. versionadded:: 1.5.0
:param _sobject: The Knowledge sObject to validate
:type _sobject: str, None
:param _use_knowledge_articles_endpoint: Determines if the ``knowledgeArticles`` endpoint should be used rather
than ``sobjects`` to retrieve the article details
:type _use_knowledge_articles_endpoint: bool, None
:returns: The provided sObject (or the default Knowledge sObject)
:raises: :py:exc:`TypeError`,
:py:exc:`salespyforce.errors.exceptions.DataMismatchError`
"""
# Ensure that the sObject is a string
if _sobject and not isinstance(_sobject, str):
exc_msg = f'The sobject must be a string (provided: {type(_sobject)})'
logger.error(exc_msg)
raise TypeError(exc_msg)
# Ensure there are no conflicting parameters
if _sobject and _use_knowledge_articles_endpoint:
if _sobject == const.SOBJECTS.KNOWLEDGE:
_info_msg = (f'It is not necessary to define the sObject as {const.SOBJECTS.KNOWLEDGE} when leveraging '
'the knowledgeArticles endpoint')
logger.info(_info_msg)
else:
_error_msg = 'You cannot use the knowledgeArticles endpoint with an explicitly defined sObject'
logger.error(_error_msg)
raise errors.exceptions.DataMismatchError(_error_msg)
# Leverage the default sObject (Knowledge__kav) if a specific sObject was not provided
elif not _sobject:
_sobject = const.SOBJECTS.KNOWLEDGE
logger.debug(const._LOG_MESSAGES._DEFAULT_SOBJECT_USED.format(sobject=_sobject))
return _sobject
def _validate_article_data(_article_data: Optional[dict] = None, _required: bool = False) -> None:
"""This function validates the article data to ensure it is defined when required and hsa the appropriate type.
.. versionadded:: 1.5.0
:param _article_data: The article data to validate
:type _article_data: dict, None
:param _required: Indicates whether the article data is required (``False`` by default)
:type _required: bool
:returns: None
:raises: :py:exc:`TypeError`,
:py:exc:`salespyforce.errors.exceptions.DataMismatchError`
"""
if _required and not _article_data:
_error_msg = const._LOG_MESSAGES._MISSING_REQUIRED_DATA.format(data='article data')
logger.error(_error_msg)
raise errors.exceptions.MissingRequiredDataError(_error_msg)
elif _article_data and not isinstance(_article_data, dict):
logger.error(const._LOG_MESSAGES._ARTICLE_DATA_TYPE_ERROR)
raise TypeError(const._LOG_MESSAGES._ARTICLE_DATA_TYPE_ERROR)
def _check_required_article_fields(_article_data: dict) -> None:
"""This function checks to ensure that the fields required to create or update an article are present.
.. versionadded:: 1.5.0
:param _article_data: The article data to validate
:type _article_data: dict
:returns: None
:raises: :py:exc:`errors.exceptions.MissingRequiredDataError``
"""
for _field in const.SOBJECT_FIELDS.REQUIRED_ARTICLE_CREATE_UPDATE_FIELDS:
if _field not in _article_data:
_error_msg = const._LOG_MESSAGES._MISSING_ARTICLE_FIELD_ERROR.format(field=_field)
logger.error(_error_msg)
raise errors.exceptions.MissingRequiredDataError(_error_msg)