Source code for khoros.structures.boards

# -*- 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