# -*- coding: utf-8 -*-
"""
:Module: khoros.structures.boards
:Synopsis: This module contains functions specific to boards within the Khoros Community platform
:Usage: ``from khoros.structures import boards``
:Example: ``board_url = boards.create(khoros_object, 'my-board', 'My Board', 'forum', return_url=True)``
:Created By: Jeff Shurtliff
:Last Modified: Jeff Shurtliff
:Modified Date: 11 Sep 2023
"""
import warnings
from operator import itemgetter
from .. import api, liql, errors
from ..objects import users
from ..utils import log_utils
from . import base
# Initialize the logger for this module
logger = log_utils.initialize_logging(__name__)
VALID_DISCUSSION_STYLES = ['blog', 'contest', 'forum', 'idea', 'qanda', 'tkb']
[docs]
def create(khoros_object, board_id, board_title, discussion_style, description=None, parent_category_id=None,
hidden=None, allowed_labels=None, use_freeform_labels=None, use_predefined_labels=None,
predefined_labels=None, media_type=None, blog_authors=None, blog_author_ids=None, blog_author_logins=None,
blog_comments_enabled=None, blog_moderators=None, blog_moderator_ids=None, blog_moderator_logins=None,
one_entry_per_contest=None, one_kudo_per_contest=None, posting_date_end=None, posting_date_start=None,
voting_date_end=None, voting_date_start=None, winner_announced_date=None, full_response=None, return_id=None,
return_url=None, return_api_url=None, return_http_code=None, return_status=None, return_error_messages=None,
split_errors=False):
"""This function creates a new board within a Khoros Community environment.
.. versionchanged:: 2.5.2
Changed the functionality around the ``return_error_messages`` argument and added the ``split_errors`` argument.
.. versionadded:: 2.5.0
:param khoros_object: The core :py:class:`khoros.Khoros` object
:type khoros_object: class[khoros.Khoros]
:param board_id: The unique identifier (i.e. ``id`` field) for the new board **(Required)**
:type board_id: str
:param board_title: The title of the new board **(Required)**
:type board_title: str
:param discussion_style: Defines the board as a ``blog``, ``contest``, ``forum``, ``idea``, ``qanda`` or ``tkb``
**(Required)**
:type discussion_style: str
:param description: A brief description of the board
:type description: str, None
:param parent_category_id: The ID of the parent category (if applicable)
:type parent_category_id: str, None
:param hidden: Defines whether or not the new board should be hidden from lists and menus (disabled by default)
:type hidden: bool, None
:param allowed_labels: The type of labels allowed on the board (``freeform-only``, ``predefined-only`` or
``freeform and pre-defined``)
:type allowed_labels: str, None
:param use_freeform_labels: Determines if freeform labels should be utilized
:type use_freeform_labels: bool, None
:param use_predefined_labels: Determines if pre-defined labels should be utilized
:type use_predefined_labels: bool, None
:param predefined_labels: The pre-defined labels to utilized on the board as a list of dictionaries
.. todo:: The ability to provide labels as a simple list and optionally standardize
their format (e.g. Pascal Case, etc.) will be available in a future release.
:type predefined_labels: list, None
:param media_type: The media type associated with a contest. (``image``, ``video`` or ``story`` meaning text)
:type media_type: str, None
:param blog_authors: The approved blog authors in a blog board as a list of user data dictionaries
:type blog_authors: list, None
:param blog_author_ids: A list of User IDs representing the approved blog authors in a blog board
:type blog_author_ids: list, None
:param blog_author_logins: A list of User Logins (i.e. usernames) representing approved blog authors in a blog board
:type blog_author_logins: list, None
:param blog_comments_enabled: Determines if comments should be enabled on blog posts within a blog board
:type blog_comments_enabled: bool, None
:param blog_moderators: The designated blog moderators in a blog board as a list of user data dictionaries
:type blog_moderators: list, None
:param blog_moderator_ids: A list of User IDs representing the blog moderators in a blog board
:type blog_moderator_ids: list, None
:param blog_moderator_logins: A list of User Logins (i.e. usernames) representing blog moderators in a blog board
:type blog_moderator_logins: list, None
:param one_entry_per_contest: Defines whether a user can submit only one entry to a single contest
:type one_entry_per_contest: bool, None
:param one_kudo_per_contest: Defines whether a user can vote only once per contest
:type one_kudo_per_contest: bool, None
:param posting_date_end: The date/time when the contest is closed to submissions
:type posting_date_end: type[datetime.datetime], None
:param posting_date_start: The date/time when the voting period for a contest begins
:type posting_date_start: type[datetime.datetime], None
:param voting_date_end: The date/time when the voting period for a contest ends
:type voting_date_end: type[datetime.datetime], None
:param voting_date_start: The date/time when the voting period for a contest begins
:type voting_date_start: type[datetime.datetime], None
:param winner_announced_date: The date/time the contest winner will be announced
:type winner_announced_date: type[datetime.datetime], None
:param full_response: Determines whether the full, raw API response should be returned by the function
.. caution:: This argument overwrites the ``return_id``, ``return_url``, ``return_api_url``,
``return_http_code``, ``return_status`` and ``return_error_messages`` arguments.
:type full_response: bool, None
:param return_id: Determines if the **ID** of the new board should be returned by the function
:type return_id: bool, None
:param return_url: Determines if the **URL** of the new board should be returned by the function
:type return_url: bool, None
:param return_api_url: Determines if the **API URL** of the new board should be returned by the function
:type return_api_url: bool, None
:param return_http_code: Determines if the **HTTP Code** of the API response should be returned by the function
:type return_http_code: bool, None
:param return_status: Determines if the **Status** of the API response should be returned by the function
:type return_status: bool, None
:param return_error_messages: Determines if the **Developer Response Message** (if any) associated with the
API response should be returned by the function
:type return_error_messages: bool, None
:param split_errors: Defines whether or not error messages should be merged when applicable
:type split_errors: bool
:returns: Boolean value indicating a successful outcome (default), the full API response or one or more specific
fields defined by function arguments
:raises: :py:exc:`khoros.errors.exceptions.MissingRequiredDataError`,
:py:exc:`khoros.errors.exceptions.InvalidNodeTypeError`,
:py:exc:`ValueError`, :py:exc:`khoros.errors.exceptions.APIConnectionError`,
:py:exc:`khoros.errors.exceptions.POSTRequestError`
"""
payload = structure_payload(khoros_object, board_id, board_title, discussion_style, description, parent_category_id,
hidden, allowed_labels, use_freeform_labels, use_predefined_labels, predefined_labels,
media_type, blog_authors, blog_author_ids, blog_author_logins, blog_comments_enabled,
blog_moderators, blog_moderator_ids, blog_moderator_logins, one_entry_per_contest,
one_kudo_per_contest, posting_date_end, posting_date_start, voting_date_end,
voting_date_start, winner_announced_date)
# TODO: Add a new api.make_v2_request() function that just takes an endpoint rather than the full URL
api_url = f"{khoros_object.core['v2_base']}/boards"
headers = {'content-type': 'application/json'}
response = api.post_request_with_retries(api_url, payload, khoros_object=khoros_object, headers=headers)
return api.deliver_v2_results(response, full_response, return_id, return_url, return_api_url, return_http_code,
return_status, return_error_messages, split_errors)
[docs]
def structure_payload(khoros_object, board_id, board_title, discussion_style, description=None, parent_category_id=None,
hidden=None, allowed_labels=None, use_freeform_labels=None, use_predefined_labels=None,
predefined_labels=None, media_type=None, blog_authors=None, blog_author_ids=None,
blog_author_logins=None, blog_comments_enabled=None, blog_moderators=None,
blog_moderator_ids=None, blog_moderator_logins=None, one_entry_per_contest=None,
one_kudo_per_contest=None, posting_date_end=None, posting_date_start=None, voting_date_end=None,
voting_date_start=None, winner_announced_date=None):
"""This function structures the payload to use in a Community API v2 request involving a board.
.. versionadded:: 2.5.0
:param khoros_object: The core :py:class:`khoros.Khoros` object
:type khoros_object: class[khoros.Khoros]
:param board_id: The unique identifier (i.e. ``id`` field) for the new board **(Required)**
:type board_id: str
:param board_title: The title of the new board **(Required)**
:type board_title: str
:param discussion_style: Defines the board as a ``blog``, ``contest``, ``forum``, ``idea``, ``qanda`` or ``tkb``
**(Required)**
:type discussion_style: str
:param description: A brief description of the board
:type description: str, None
:param parent_category_id: The ID of the parent category (if applicable)
:type parent_category_id: str, None
:param hidden: Defines whether or not the new board should be hidden from lists and menus (disabled by default)
:type hidden: bool, None
:param allowed_labels: The type of labels allowed on the board (``freeform-only``, ``predefined-only`` or
``freeform and pre-defined``)
:type allowed_labels: str, None
:param use_freeform_labels: Determines if freeform labels should be utilized
:type use_freeform_labels: bool, None
:param use_predefined_labels: Determines if pre-defined labels should be utilized
:type use_predefined_labels: bool, None
:param predefined_labels: The pre-defined labels to utilized on the board as a list of dictionaries
.. todo:: The ability to provide labels as a simple list and optionally standardize
their format (e.g. Pascal Case, etc.) will be available in a future release.
:type predefined_labels: list, None
:param media_type: The media type associated with a contest. (``image``, ``video`` or ``story`` meaning text)
:type media_type: str, None
:param blog_authors: The approved blog authors in a blog board as a list of user data dictionaries
:type blog_authors: list, None
:param blog_author_ids: A list of User IDs representing the approved blog authors in a blog board
:type blog_author_ids: list, None
:param blog_author_logins: A list of User Logins (i.e. usernames) representing approved blog authors in a blog board
:type blog_author_logins: list, None
:param blog_comments_enabled: Determines if comments should be enabled on blog posts within a blog board
:type blog_comments_enabled: bool, None
:param blog_moderators: The designated blog moderators in a blog board as a list of user data dictionaries
:type blog_moderators: list, None
:param blog_moderator_ids: A list of User IDs representing the blog moderators in a blog board
:type blog_moderator_ids: list, None
:param blog_moderator_logins: A list of User Logins (i.e. usernames) representing blog moderators in a blog board
:type blog_moderator_logins: list, None
:param one_entry_per_contest: Defines whether a user can submit only one entry to a single contest
:type one_entry_per_contest: bool, None
:param one_kudo_per_contest: Defines whether a user can vote only once per contest
:type one_kudo_per_contest: bool, None
:param posting_date_end: The date/time when the contest is closed to submissions
:type posting_date_end: type[datetime.datetime], None
:param posting_date_start: The date/time when the voting period for a contest begins
:type posting_date_start: type[datetime.datetime], None
:param voting_date_end: The date/time when the voting period for a contest ends
:type voting_date_end: type[datetime.datetime], None
:param voting_date_start: The date/time when the voting period for a contest begins
:type voting_date_start: type[datetime.datetime], None
:param winner_announced_date: The date/time the contest winner will be announced
:type winner_announced_date: type[datetime.datetime], None
:returns: The full and properly formatted payload for the API request
:raises: :py:exc:`khoros.errors.exceptions.MissingRequiredDataError`,
:py:exc:`khoros.errors.exceptions.InvalidNodeTypeError`
"""
# Define the minimum payload framework
payload = {
"data": {
"type": "board"
}
}
# Populate relevant sections of the payload
required_error_msg = "The 'board_id', 'board_title' and 'discussion_style' fields are required to create a board."
payload = _structure_id_and_title(board_id, board_title, payload, required_error_msg)
payload = _structure_discussion_style(discussion_style, payload, required_error_msg)
payload = _structure_parent_category(parent_category_id, payload)
# Populate the remaining simple fields
simple_fields = {
'description': description,
'hidden': hidden,
'media_type': media_type,
}
payload = _structure_simple_fields(simple_fields, payload)
# Populate the label settings
label_settings = {
'allowed_labels': allowed_labels,
'use_freeform_labels': use_freeform_labels,
'use_predefined_labels': use_predefined_labels,
'predefined_labels': predefined_labels,
}
payload = _structure_label_settings(label_settings, payload)
# Populate the blog-related settings
blog_settings = {
'authors': blog_authors,
'author_ids': blog_author_ids,
'author_logins': blog_author_logins,
'comments_enabled': blog_comments_enabled,
'moderators': blog_moderators,
'moderator_ids': blog_moderator_ids,
'moderator_logins': blog_moderator_logins,
}
payload = _structure_blog_settings(khoros_object, blog_settings, payload, discussion_style)
# Populate the contest-related settings
contest_settings = {
'one_entry_per_contest': one_entry_per_contest,
'one_kudo_per_contest': one_kudo_per_contest,
'posting_date_end': posting_date_end,
'posting_date_start': posting_date_start,
'voting_date_end': voting_date_end,
'voting_date_start': voting_date_start,
'winner_announced_date': winner_announced_date,
}
payload = _structure_contest_settings(contest_settings, payload, discussion_style)
return payload
def _structure_id_and_title(_board_id, _board_title, _payload, _missing_error):
"""This function structures the portion of the payload for the Board ID and Board Title.
.. versionadded:: 2.5.0
:param _board_id: The unique identifier (i.e. ``id`` field) for the new board **(Required)**
:type _board_id: str
:param _board_title: The title of the new board **(Required)**
:type _board_title: str
:param _payload: The partially constructed payload for the API request
:type _payload: dict
:param _missing_error: The error message to use when raising the
:py:exc:`khoros.errors.exceptions.MissingRequiredDataError` exception class when the
Board ID and/or Board Title have not been provided.
:returns: The API request payload with appended entries for ``id`` and ``title``
:raises: :py:exc:`khoros.errors.exceptions.MissingRequiredDataError`
"""
if not _board_id or not _board_title:
raise errors.exceptions.MissingRequiredDataError(_missing_error)
_payload['data']['id'] = _board_id
_payload['data']['title'] = _board_title
return _payload
def _structure_discussion_style(_discussion_style, _payload, _missing_error):
"""This function structures the portion of the payload for the Discussion Style. (i.e. board type)
.. versionadded:: 2.5.0
:param _discussion_style: Defines the board as a ``blog``, ``contest``, ``forum``, ``idea``, ``qanda`` or ``tkb``
**(Required)**
:type _discussion_style: str
:param _payload: The partially constructed payload for the API request
:type _payload: dict
:param _missing_error: The error message to use when raising the
:py:exc:`khoros.errors.exceptions.MissingRequiredDataError` exception class when the
Discussion Style have not been provided.
:returns: The API request payload with appended entries for ``conversation_style``
:raises: :py:exc:`khoros.errors.exceptions.MissingRequiredDataError`,
:py:exc:`khoros.errors.exceptions.InvalidNodeTypeError`
"""
if not _discussion_style:
raise errors.exceptions.MissingRequiredDataError(_missing_error)
if _discussion_style not in VALID_DISCUSSION_STYLES:
raise errors.exceptions.InvalidNodeTypeError(f"'{_discussion_style}' is not a valid discussion style.")
_payload['data']['conversation_style'] = _discussion_style
return _payload
def _structure_parent_category(_parent_id, _payload):
"""This function structures the portion of the payload for the parent category.
.. versionadded:: 2.5.0
:param _parent_id: The ID of the parent category (if applicable)
:type _parent_id: str, None
:param _payload: The partially constructed payload for the API request
:type _payload: dict
:returns: The API request payload with appended entries for ``parent_category``
"""
if _parent_id:
_payload['data']['parent_category'] = {
'id': _parent_id
}
return _payload
def _structure_simple_fields(_simple_fields, _payload):
"""This function structures the portion of the payload for the simple string and Boolean fields.
.. versionadded:: 2.5.0
:param _simple_fields: A dictionary containing the simple string and Boolean fields for the API payload
:type _simple_fields: dict
:param _payload: The partially constructed payload for the API request
:type _payload: dict
:returns: The API request payload with appended entries for the simple string and Boolean fields
"""
for _field, _value in _simple_fields.items():
if _value:
_payload['data'][_field] = _value
return _payload
def _structure_label_settings(_label_settings, _payload):
"""This function structures the portion of the payload for the setting fields relating to labels.
.. versionadded:: 2.5.0
:param _label_settings: A dictionary containing the settings involving labels
:type _label_settings: dict
:param _payload: The partially constructed payload for the API request
:type _payload: dict
:returns: The API request payload with appended entries for the field settings relating to labels
"""
# Verify that any string passed in 'allowed_labels' is valid when applicable
_valid_allowed_labels = ['freeform-only', 'predefined-only', 'freeform and pre-defined']
if _label_settings.get('allowed_labels'):
if _label_settings.get('allowed_labels') in _valid_allowed_labels:
_payload['data']['allowed_labels'] = _label_settings.get('allowed_labels')
else:
# TODO: Leverage the logger instead of warnings
warn_msg = f"The string '{_label_settings.get('allowed_labels')}' for the 'allowed_labels' field is " \
"not valid and will be ignored."
warnings.warn(warn_msg, UserWarning)
_label_settings['allowed_labels'] = None
# Inform the user that the value will be overwritten by any defined Boolean label settings values
if _label_settings.get('allowed_labels') and (
_label_settings.get('allowed_labels') is not None or
_label_settings.get('allowed_labels') is not None):
# TODO: Leverage the logger instead of warnings
warn_msg = "The defined 'allowed_labels' field will be overwritten when the 'use_freeform_labels' and/or " \
"'use_predefined_labels' Boolean values are also configured."
warnings.warn(warn_msg, UserWarning)
# Define the 'allowed_labels' value based on the defined Boolean values when applicable
_boolean_values = (_label_settings.get('use_freeform_labels'), _label_settings.get('use_predefined_labels'))
if any(_boolean_values):
if all(_boolean_values):
_payload['data']['allowed_labels'] = 'freeform and pre-defined'
elif _label_settings.get('use_freeform_labels'):
_payload['data']['allowed_labels'] = 'freeform-only'
elif _label_settings.get('use_predefined_labels'):
_payload['data']['allowed_labels'] = 'predefined-only'
# Add the predefined labels if present
# TODO: Check to ensure that they are in the proper format (In Studio they are a comma-separated list)
# TODO: Provide the option to format the labels into all lowercase or Pascal Case
# TODO: Set up unit test to see what happens if 'use_predefined_labels' is True but no labels are predefined
if _label_settings.get('predefined_labels'):
_payload['data']['predefined_labels'] = _label_settings.get('predefined_labels')
return _payload
def _structure_blog_settings(_khoros_object, _blog_settings, _payload, _discussion_style):
"""This function structures the portion of the payload for the setting fields relating to blogs.
.. versionadded:: 2.5.0
:param _khoros_object:
:param _blog_settings: A dictionary containing the settings involving labels
:type _blog_settings: dict
:param _payload: The partially constructed payload for the API request
:type _payload: dict
:param _discussion_style: The discussion style for the new board to ensure that these settings apply
:type _discussion_style: str
:returns: The API request payload with appended entries for the field settings relating to labels
"""
if any(_blog_settings.values()) and _discussion_style != 'blog':
_warn_about_ignored_settings('blog', _discussion_style)
else:
# Populate the 'comments_enabled' setting if applicable
if _blog_settings.get('comments_enabled'):
_payload['data']['comments_enabled'] = _blog_settings.get('comments_enabled')
# Populate the blog authors list
if any((_blog_settings['authors'], _blog_settings['author_ids'], _blog_settings['author_logins'])):
authors = users.structure_user_dict_list(_khoros_object, _blog_settings['authors'],
_blog_settings['author_ids'], _blog_settings['author_logins'])
_payload['data']['authors'] = authors
# Populate the blog moderators list
if any((_blog_settings['moderators'], _blog_settings['moderator_ids'], _blog_settings['moderator_logins'])):
moderators = users.structure_user_dict_list(_khoros_object, _blog_settings['moderators'],
_blog_settings['moderator_ids'],
_blog_settings['moderator_logins'])
_payload['data']['moderators'] = moderators
return _payload
def _structure_contest_settings(_contest_settings, _payload, _discussion_style):
"""This function structures the portion of the payload for the setting fields relating to contests.
.. versionadded:: 2.5.0
:param _contest_settings: A dictionary containing the settings involving contests
:type _contest_settings: dict
:param _payload: The partially constructed payload for the API request
:type _payload: dict
:param _discussion_style: The discussion style for the new board to ensure that these settings apply
:type _discussion_style: str
:returns: The API request payload with appended entries for the field settings relating to contests
"""
if any(_contest_settings.values()) and _discussion_style != 'contest':
_warn_about_ignored_settings('contest', _discussion_style)
else:
for _field, _value in _contest_settings.items():
if _value:
# TODO: Verify proper format for datetime values
_payload['data'][_field] = _value
return _payload
def _warn_about_ignored_settings(_settings_type, _discussion_style):
"""This function displays a ``UserWarning`` that provided fields will be ignored if the discussion style does not
match the style for which the field are specific.
.. versionchanged:: 5.0.0
Removed the redundant return statement.
.. versionadded:: 2.5.0
:param _settings_type: The discussion style relating to the supplied fields and values
:type _settings_type: str
:param _discussion_style: The discussion style of the new board
:type _discussion_style: str
:returns: None
"""
# TODO: Leverage the logger instead of warnings
warn_msg = f"Because the discussion style is '{_discussion_style}' all {_settings_type}-specific fields " \
"provided will be ignored."
warnings.warn(warn_msg, UserWarning)
[docs]
def get_board_id(url):
"""This function retrieves the Board ID for a given board when provided its URL.
.. versionadded:: 2.6.0
:param url: The URL from which to parse out the Board ID
:type url: str
:returns: The Board ID retrieved from the URL
:raises: :py:exc:`khoros.errors.exceptions.InvalidURLError`
"""
return base.get_structure_id(url)
[docs]
def board_exists(khoros_object, board_id=None, board_url=None):
"""This function checks to see if a board (i.e. blog, contest, forum, idea exchange, Q&A or TKB) exists.
.. versionadded:: 2.7.0
:param khoros_object: The core :py:class:`khoros.Khoros` object
:type khoros_object: class[khoros.Khoros]
:param board_id: The ID of the board to check
:type board_id: str, None
:param board_url: The URL of the board to check
:type board_url: str, None
:returns: Boolean value indicating whether the board already exists
:raises: :py:exc:`khoros.errors.exceptions.MissingRequiredDataError`
"""
return base.structure_exists(khoros_object, 'board', board_id, board_url)
[docs]
def get_message_count(khoros_object, board_id):
"""This function retrieves the total number of messages within a given board.
.. versionadded:: 5.3.0
:param khoros_object: The core :py:class:`khoros.Khoros` object
:type khoros_object: class[khoros.Khoros]
:param board_id: The ID of the board to query
:type board_id: str
:returns: The number of messages within the node
"""
query = f"SELECT count(*) FROM messages WHERE board.id = '{board_id}'"
message_count = khoros_object.query(query).get('data').get('count')
return message_count
[docs]
def get_all_messages(khoros_object, board_id, fields=None, where_filter=None, descending=True):
"""This function retrieves data for all messages within a given board.
.. versionchanged:: 5.4.0
Introduced the ``where_filter`` and ``descending`` parameters to more specifically adjust the LiQL query.
.. versionadded:: 5.3.0
:param khoros_object: The core :py:class:`khoros.Khoros` object
:type khoros_object: class[khoros.Khoros]
:param board_id: The ID of the board to query
:type board_id: str
:param fields: Specific fields to query if not all fields are needed (comma-separated string or iterable)
:type fields: str, tuple, list, set, None
:param where_filter: One or more optional WHERE filters to include in the LiQL query
:type where_filter: str, tuple, list, set, None
:param descending: Determines if the data should be returned in descending order (``True`` by default)
:type descending: bool
:returns: A list containing a dictionary of data for each message within the board
:raises: :py:exc:`khoros.errors.exceptions.GETRequestError`
"""
# Define the variable where the messages will be stored
messages = []
# Define the LiQL query to be performed
fields = '*' if not fields else fields
if not isinstance(fields, str):
fields = liql.parse_select_fields(fields)
where_clause = _construct_where_clause(where_filter)
order_by_clause = _construct_order_by_clause(where_filter, descending)
query = f"SELECT {fields} FROM messages WHERE board.id = '{board_id}' " \
f"{where_clause}ORDER BY {order_by_clause} LIMIT 1000"
# Perform the first LiQL query and add to the master list
response, cursor = _perform_single_query(khoros_object, query, fields)
messages.extend(response)
# Continue looping as long as a cursor is present
while cursor:
response, cursor = _perform_single_query(khoros_object, query, fields, cursor)
messages.extend(response)
# Return the collected messages
return messages
[docs]
def get_all_topic_messages(khoros_object, board_id, fields=None, descending=True):
"""This function retrieves data for all topic messages (i.e. zero-depth messages) within a given board.
.. versionadded:: 5.4.0
:param khoros_object: The core :py:class:`khoros.Khoros` object
:type khoros_object: class[khoros.Khoros]
:param board_id: The ID of the board to query
:type board_id: str
:param fields: Specific fields to query if not all fields are needed (comma-separated string or iterable)
:type fields: str, tuple, list, set, None
:param descending: Determines if the data should be returned in descending order (``True`` by default)
:type descending: bool
:returns: A list containing a dictionary of data for each topic message within the board
:raises: :py:exc:`khoros.errors.exceptions.GETRequestError`
"""
return get_all_messages(khoros_object, board_id, fields, where_filter='depth=0', descending=descending)
def _construct_where_clause(_where_filter=None):
"""This function constructs a supplemental WHERE clause from any filters that are provided.
.. versionadded:: 5.4.0
:param _where_filter: Zero or more WHERE filters
:type _where_filter: str, list, set, tuple, None
:returns: The constructed supplemental WHERE clause as a string
"""
_where_clause = ''
if _where_filter:
_where_filter = [_where_filter] if isinstance(_where_filter, str) else _where_filter
for _filter in _where_filter:
if _filter:
_where_clause += f'AND {_filter} '
return _where_clause
def _construct_order_by_clause(_where_filter=None, _descending=True):
"""This function constructs the ORDER BY clause for the :py:func:`khoros.objects.boards.get_all_messages` function.
.. versionadded:: 5.4.0
:param _where_filter: Zero or more WHERE filters
:type _where_filter: str, list, set, tuple, None
:param _descending: Determines if the data should be returned in descending order (``True`` by default)
:type _descending: bool
:returns: The constructed ORDER BY clause
"""
_field = 'last_publish_time'
if isinstance(_where_filter, str) and 'depth=0' in _where_filter:
_field = 'conversation.last_post_time'
_direction = 'DESC' if _descending else 'ASC'
return f'{_field} {_direction}'
def _perform_single_query(khoros_object, query, fields=None, cursor=None):
"""This function performs a single LiQL query with or without a cursor.
.. versionadded:: 5.3.0
:param khoros_object: The core :py:class:`khoros.Khoros` object
:type khoros_object: class[khoros.Khoros]
:param query: The LiQL query to be performed
:type query: str
:param fields: Specific fields used in the LiQL SELECT statement
:type fields: str, tuple, list, set, None
:param cursor: The cursor from the LiQL response (when present)
:type cursor: str, None
:returns: The response to the LiQL query and the cursor when applicable
:raises: :py:exc:`khoros.errors.exceptions.GETRequestError`
"""
# Construct the entire LiQL query
cursor = '' if not cursor else liql.structure_cursor_clause(cursor)
query = f"{query} {cursor}" if cursor else query
# Perform the API call and retrieve the data
response = liql.perform_query(khoros_object, liql_query=query)
data = liql.get_returned_items(response)
# Get the cursor when present
cursor = None
if response.get('data').get('next_cursor'):
cursor = response['data'].get('next_cursor')
# Add missing columns to message data as needed
data = _add_missing_cols(data, fields)
try:
data = sorted(data, key=itemgetter(*tuple(data[0].keys())))
except KeyError as missing_key:
logger.error(f'Could not sort the message data because the \'{missing_key}\' key was missing.')
# Return the user data and cursor
return data, cursor
def _add_missing_cols(msg_list, fields=None):
"""This function adds columns (fields) that might be missing from a LiQL response containing messages.
.. versionadded:: 5.3.0
:param msg_list: The list of dictionaries containing message data
:type msg_list: list
:param fields: Specific fields used in the LiQL SELECT statement
:type fields: str, tuple, list, set, None
:returns: The same Liql response data with the required columns included
"""
new_list = []
required_cols = ['type', 'id', 'view_href', 'subject', 'last_publish_time']
# Add any defined fields to the list of required columns
if fields and fields != '*':
parsed_fields = fields.split(',')
for field in parsed_fields:
if field not in required_cols:
required_cols.append(field)
# Loop through the messages and add any missing columns
for msg in msg_list:
for col in required_cols:
if col not in msg:
msg[col] = ''
new_list.append(msg)
return new_list