예제 #1
0
 /// <summary>
 /// Constructor
 /// </summary>
 /// <param name="onlineUrls"></param>
 /// <param name="login"></param>
 /// <param name="workbook"></param>
 /// <param name="localSavePath"></param>
 public DownloadWorkbookThumbnail(
     TableauServerUrls onlineUrls,
     TableauServerSignIn login,
     SiteWorkbook workbook,
     string localSavePath)
     : base(login)
 {
     _onlineUrls        = onlineUrls;
     _workbook          = workbook;
     _localSavePathRoot = localSavePath;
 }
예제 #2
0
    /// <summary>
    /// URL to download a workbook's view
    /// </summary>
    /// <param name="siteUrlSegment"></param>
    /// <returns></returns>
    public string Url_WorkbookViewThumbnailDownload(TableauServerSignIn session, SiteWorkbook contentInfo, string viewId)
    {
        string workingText = _urlDownloadWorkbookViewThumbnailTemplate;

        workingText = workingText.Replace("{{iwsSiteId}}", session.SiteId);
        workingText = workingText.Replace("{{iwsRepositoryId}}", contentInfo.Id);
        workingText = workingText.Replace("{{iwsViewId}}", viewId);

        ValidateTemplateReplaceComplete(workingText);
        return(workingText);
    }
예제 #3
0
 private SiteWorkbook ParseWorkbookElement(XElement element)
 {
     try
     {
         var workbookXml = element.ToXmlNode();
         var workbook    = new SiteWorkbook(workbookXml, xmlNamespace);
         return(workbook);
     }
     catch (Exception e)
     {
         Console.WriteLine(e);
         throw;
     }
 }
/// <summary>
/// Add data source connection data
/// </summary>
/// <param name="thisWorkbook"></param>
    private void AddWorkbookConnectionData(SiteWorkbook thisWorkbook)
    {
        var dataConnections = thisWorkbook.DataConnections;

        if (dataConnections == null)
        {
            return;
        }

        //Write out details for each data connection
        foreach (var thisConnection in dataConnections)
        {
            this.AddKeyValuePairs(
                new string[] {
                ContentType                 //1
                , ContentId                 //2
                , ContentConnectionType     //3
                , ContentConnectionServer   //4
                , ContentConnectionPort     //5
                , ContentConnectionUserName //6
                , ContentWorkbookId         //7
                , ContentWorkbookName       //8
                , ContentProjectId          //9
                , ContentProjectName        //10
                , ContentOwnerId            //11
                , DeveloperNotes            //12
            },
                new string[] {
                "data-connection"               //1
                , thisConnection.Id             //2
                , thisConnection.ConnectionType //3
                , thisConnection.ServerAddress  //4
                , thisConnection.ServerPort     //5
                , thisConnection.UserName       //6
                , thisWorkbook.Id               //7
                , thisWorkbook.Name             //8
                , thisWorkbook.ProjectId        //9
                , thisWorkbook.ProjectName      //10
                , thisWorkbook.OwnerId          //11
                , thisWorkbook.DeveloperNotes   //12
            });
        }
    }
예제 #5
0
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="onlineUrls"></param>
    /// <param name="login"></param>
    /// <param name="workbook"></param>
    /// <param name="localSavePath"></param>
    public DownloadWorkbookViewThumbnail(
        TableauServerUrls onlineUrls,
        TableauServerSignIn login,
        SiteWorkbook workbook,
        string viewId,
        string localSavePath)
        : base(login)
    {
        if (string.IsNullOrWhiteSpace(viewId))
        {
            var errorText = "Download workbook view thumbnail, viewId cannot be blank.  Workbook: " + workbook.Id;
            login.StatusLog.AddError(errorText);
            throw new Exception(errorText);
        }

        _onlineUrls        = onlineUrls;
        _workbook          = workbook;
        _localSavePathRoot = localSavePath;
        _viewId            = viewId;
    }
예제 #6
0
        /// <summary>
        /// If we have Project Mapping information, generate a project based path for the download
        /// </summary>
        /// <param name="basePath">File system location which will be the root of project paths.</param>
        /// <param name="workbook">Workbook record.</param>
        /// <param name="statusLog">Logging object.</param>
        /// <returns></returns>
        public static string EnsureProjectBasedPath(string basePath, SiteWorkbook workbook, TaskStatusLogs statusLog)
        {
            //If we have no project list to do lookups in then just return the base path
            if (workbook == null)
            {
                return(basePath);
            }

            //Turn the project name into a directory name
            var safeDirectoryName = GenerateWindowsSafeFilename(workbook.Name);

            var pathWithProject = Path.Combine(basePath, safeDirectoryName);

            //If needed, create the directory
            if (!Directory.Exists(pathWithProject))
            {
                Directory.CreateDirectory(pathWithProject);
            }

            return(pathWithProject);
        }
    /// <summary>
    /// Build a list of Workbooks
    /// </summary>
    /// <param name="xmlWorkbooksList"></param>
    /// <returns></returns>
    private List <SiteWorkbook> GenerateWorkbooksList(XmlNodeList xmlWorkbooksList)
    {
        var onlineWorkbooks = new List <SiteWorkbook>();

        //Get information for each of the data sources
        foreach (XmlNode itemXml in xmlWorkbooksList)
        {
            try
            {
                var thisItem = new SiteWorkbook(itemXml);
                onlineWorkbooks.Add(thisItem);
            }
            catch
            {
                AppDiagnostics.Assert(false, "1024-1113: Workbook parse error");
                _onlineSession.StatusLog.AddError("1024-1113: Error parsing workbook: " + itemXml.InnerXml);
            }
        } //end: foreach

        return(onlineWorkbooks);
    }
    /// <summary>
    /// Downloads a single request for 1 workbook
    /// </summary>
    /// <param name="workbookId"></param>
    /// <returns></returns>
    public SiteWorkbook ExecuteRequest_SingleWorkbook(string workbookId)
    {
        //Sanity check
        if (string.IsNullOrWhiteSpace(workbookId))
        {
            _onlineSession.StatusLog.AddError("Workbook ID required to query workbooks");
        }
        //Create a web request, in including the users logged-in auth information in the request headers
        var urlQuery = _onlineUrls.Url_WorkbookInfo(_onlineSession, workbookId);

        _onlineSession.StatusLog.AddStatus("Web request: " + urlQuery, -10);
        var xmlDoc = ResourceSafe_PerformWebRequest_GetXmlDocument(urlQuery, "get workbooks list");
        //var webRequest = CreateLoggedInWebRequest(urlQuery);
        //webRequest.Method = "GET";
        //var response = GetWebReponseLogErrors(webRequest, "get workbooks list");
        //var xmlDoc = GetWebResponseAsXml(response);

        //Get all the workbook nodes
        var nsManager = XmlHelper.CreateTableauXmlNamespaceManager("iwsOnline");
        var workbooks = xmlDoc.SelectNodes("//iwsOnline:workbook", nsManager);

        //Get information for each of the data sources
        foreach (XmlNode itemXml in workbooks)
        {
            try
            {
                var wb = new SiteWorkbook(itemXml);
                //There is ONLY one workbook, so return it
                return(wb);
            }
            catch
            {
                AppDiagnostics.Assert(false, "Workbook parse error");
                _onlineSession.StatusLog.AddError("Error parsing workbook: " + itemXml.InnerXml);
                return(null);
            }
        } //end: foreach

        return(null);
    }
    /// <summary>
    /// Save Workbook metadata in a XML file along-side the workbook file
    /// </summary>
    /// <param name="wb">Information about the workbook we have downloaded</param>
    /// <param name="localWorkbookPath">Local path to the twb/twbx of the workbook</param>
    /// <param name="userLookups">If non-NULL contains the mapping of users/ids so we can look up the owner</param>
    internal static void CreateSettingsFile(SiteWorkbook wb, string localWorkbookPath, KeyedLookup<SiteUser> userLookups)
    {
        string contentOwnerName = null; //Start off assuming we have no content owner information
        if(userLookups != null)
        {
            contentOwnerName = helper_LookUpOwnerId(wb.OwnerId, userLookups);
        }

        var xml = System.Xml.XmlWriter.Create(PathForSettingsFile(localWorkbookPath));
        xml.WriteStartDocument();
            xml.WriteStartElement(XmlElement_WorkbookInfo);
            XmlHelper.WriteValueElement(xml, XmlElement_ShowTabsInWorkbook, wb.ShowTabs);

                //If we have an owner name, write it out
                if (!string.IsNullOrWhiteSpace(contentOwnerName))
                {
                  XmlHelper.WriteValueElement(xml, XmlElement_ContentOwner, contentOwnerName);
                }
            xml.WriteEndElement(); //end: WorkbookInfo
        xml.WriteEndDocument();
        xml.Close();
    }
    /// <summary>
    /// Get a page's worth of Workbook listings
    /// </summary>
    /// <param name="onlineWorkbooks"></param>
    /// <param name="pageToRequest">Page # we are requesting (1 based)</param>
    /// <param name="totalNumberPages">Total # of pages of data that Server can return us</param>
    private void ExecuteRequest_ForPage(List <SiteWorkbook> onlineWorkbooks, int pageToRequest, out int totalNumberPages)
    {
        int pageSize = _onlineUrls.PageSize;
        //Create a web request, in including the users logged-in auth information in the request headers
        var urlQuery = _onlineUrls.Url_WorkbooksListForUser(_onlineSession, _userIdForWorkbookQuery, pageSize, pageToRequest, SortDirectiveForContentQuery());

        _onlineSession.StatusLog.AddStatus("Web request: " + urlQuery, -10);
        var xmlDoc = ResourceSafe_PerformWebRequest_GetXmlDocument(urlQuery, "get workbooks list");
        //var webRequest = CreateLoggedInWebRequest(urlQuery);
        //webRequest.Method = "GET";
        //var response = GetWebReponseLogErrors(webRequest, "get workbooks list");
        //var xmlDoc = GetWebResponseAsXml(response);

        //Get all the workbook nodes
        var nsManager = XmlHelper.CreateTableauXmlNamespaceManager("iwsOnline");
        var workbooks = xmlDoc.SelectNodes("//iwsOnline:workbook", nsManager);

        //Get information for each of the data sources
        foreach (XmlNode itemXml in workbooks)
        {
            try
            {
                var ds = new SiteWorkbook(itemXml);
                onlineWorkbooks.Add(ds);
            }
            catch
            {
                AppDiagnostics.Assert(false, "Workbook parse error");
                _onlineSession.StatusLog.AddError("Error parsing workbook: " + itemXml.InnerXml);
            }
        } //end: foreach

        //-------------------------------------------------------------------
        //Get the updated page-count
        //-------------------------------------------------------------------
        totalNumberPages = DownloadPaginationHelper.GetNumberOfPagesFromPagination(
            xmlDoc.SelectSingleNode("//iwsOnline:pagination", nsManager),
            pageSize);
    }
예제 #11
0
    /// <summary>
    /// Save Workbook metadata in a XML file along-side the workbook file
    /// </summary>
    /// <param name="wb">Information about the workbook we have downloaded</param>
    /// <param name="localWorkbookPath">Local path to the twb/twbx of the workbook</param>
    /// <param name="userLookups">If non-NULL contains the mapping of users/ids so we can look up the owner</param>
    internal static void CreateSettingsFile(SiteWorkbook wb, string localWorkbookPath, KeyedLookup <SiteUser> userLookups)
    {
        string contentOwnerName = null; //Start off assuming we have no content owner information

        if (userLookups != null)
        {
            contentOwnerName = helper_LookUpOwnerId(wb.OwnerId, userLookups);
        }

        var xml = System.Xml.XmlWriter.Create(PathForSettingsFile(localWorkbookPath));

        xml.WriteStartDocument();
        xml.WriteStartElement(XmlElement_WorkbookInfo);
        XmlHelper.WriteValueElement(xml, XmlElement_ShowTabsInWorkbook, wb.ShowTabs);

        //If we have an owner name, write it out
        if (!string.IsNullOrWhiteSpace(contentOwnerName))
        {
            XmlHelper.WriteValueElement(xml, XmlElement_ContentOwner, contentOwnerName);
        }
        xml.WriteEndElement();     //end: WorkbookInfo
        xml.WriteEndDocument();
        xml.Close();
    }
    /// <summary>
    /// Get a page's worth of Workbook listings
    /// </summary>
    /// <param name="onlineWorkbooks"></param>
    /// <param name="pageToRequest">Page # we are requesting (1 based)</param>
    /// <param name="totalNumberPages">Total # of pages of data that Server can return us</param>
    private void ExecuteRequest_ForPage(List<SiteWorkbook> onlineWorkbooks, int pageToRequest, out int totalNumberPages)
    {
        int pageSize = _onlineUrls.PageSize;
        //Create a web request, in including the users logged-in auth information in the request headers
        var urlQuery = _onlineUrls.Url_WorkbooksListForUser(_onlineSession, _userId, pageSize, pageToRequest);
        var webRequest = CreateLoggedInWebRequest(urlQuery);
        webRequest.Method = "GET";

        _onlineSession.StatusLog.AddStatus("Web request: " + urlQuery, -10);
        var response = GetWebReponseLogErrors(webRequest, "get workbooks list");
        var xmlDoc = GetWebResponseAsXml(response);

        //Get all the workbook nodes
        var nsManager = XmlHelper.CreateTableauXmlNamespaceManager("iwsOnline");
        var workbooks = xmlDoc.SelectNodes("//iwsOnline:workbook", nsManager);

        //Get information for each of the data sources
        foreach (XmlNode itemXml in workbooks)
        {
            try
            {
                var ds = new SiteWorkbook(itemXml);
                onlineWorkbooks.Add(ds);
            }
            catch
            {
                AppDiagnostics.Assert(false, "Workbook parse error");
                _onlineSession.StatusLog.AddError("Error parsing workbook: " + itemXml.InnerXml);
            }
        } //end: foreach

        //-------------------------------------------------------------------
        //Get the updated page-count
        //-------------------------------------------------------------------
        totalNumberPages = DownloadPaginationHelper.GetNumberOfPagesFromPagination(
            xmlDoc.SelectSingleNode("//iwsOnline:pagination", nsManager),
            pageSize);
    }
예제 #13
0
    /// <summary>
    /// URL to download a workbook
    /// </summary>
    /// <param name="siteUrlSegment"></param>
    /// <returns></returns>
    public string Url_WorkbookDownload(TableauServerSignIn session, SiteWorkbook contentInfo)
    {
        string workingText = _urlDownloadWorkbookTemplate;
        workingText = workingText.Replace("{{iwsSiteId}}", session.SiteId);
        workingText = workingText.Replace("{{iwsRepositoryId}}", contentInfo.Id);

        ValidateTemplateReplaceComplete(workingText);
        return workingText;
    }
예제 #14
0
    /// <summary>
    /// Assign ownership
    /// </summary>
    /// <param name="workbook"></param>
    /// <param name="publishSettings"></param>
    /// <param name="siteUsers"></param>
    /// <returns>TRUE: The server content has the correct owner now.  FALSE: We were unable to give the server content the correct owner</returns>
    private bool AttemptOwnerReassignment(SiteWorkbook workbook, WorkbookPublishSettings publishSettings, IEnumerable <SiteUser> siteUsers)
    {
        this.StatusLog.AddStatusHeader("Attempting ownership assignement for Workbook " + workbook.Name + "/" + workbook.Id);

        //Something went wrong if we don't have a set of site users to do the look up
        if (siteUsers == null)
        {
            throw new ArgumentException("No set of site users provided for lookup");
        }

        //Look the local meta data to see what the desired name is
        var desiredOwnerName = publishSettings.OwnerName;

        if (string.IsNullOrEmpty(desiredOwnerName))
        {
            this.StatusLog.AddStatus("Skipping owner assignment. The local file system has no metadata with an owner information for " + workbook.Name);
            LogManualAction_ReassignOwnership(workbook.Name, "none specified", "No client ownership information was specified");
            return(true); //Since there is no ownership preference stated locally, then ownership we assigned during upload was fine.
        }

        //Look on the list of users in the target site/server, and try to find a match
        //
        //NOTE: We are doing a CASE INSENSITIVE name comparison. We assume that there are not 2 users with the same name on server w/differet cases
        //      Because if this, we want to be flexible and allow that our source/destination servers may have the user name specified with a differnt
        //      case.  -- This is less secure than a case-sensitive comparison, but is almost always what we want when porting content between servers
        var desiredServerUser = SiteUser.FindUserWithName(siteUsers, desiredOwnerName, StringComparison.InvariantCultureIgnoreCase);

        if (desiredServerUser == null)
        {
            this.StatusLog.AddError("The local file has a workbook/user mapping: " + workbook.Name + "/" + desiredOwnerName + ", but this user does not exist on the target site");
            LogManualAction_ReassignOwnership(workbook.Name, desiredOwnerName, "The target site does not contain a user name that matches the owner specified by the local metadata");
            return(false); //Not a run time error, but we have manual steps to perform
        }

        //If the server content is already correct, then there is nothing to do
        if (desiredServerUser.Id == workbook.OwnerId)
        {
            this.StatusLog.AddStatus("Workbook " + workbook.Name + "/" + workbook.Id + ", already has correct ownership. No update requried");
            return(true);
        }

        //Lets tell server to update the owner
        var          changeOwnership = new SendUpdateWorkbookOwner(_onlineSession, workbook.Id, desiredServerUser.Id);
        SiteWorkbook updatedWorkbook;

        try
        {
            this.StatusLog.AddStatus("Server request to change Workbook ownership, wb: " + workbook.Name + "/" + workbook.Id + ", user:"******"/" + desiredServerUser.Id);
            updatedWorkbook = changeOwnership.ExecuteRequest();
        }
        catch (Exception exChangeOnwnerhsip)
        {
            throw exChangeOnwnerhsip; //Unexpected error, send it upward
        }

        //Sanity check the result we got back: Double check to make sure we have the expected owner.
        if (updatedWorkbook.OwnerId != desiredServerUser.Id)
        {
            this.StatusLog.AddError("Unexpected server error! Updated workbook Owner Id does not match expected. wb: "
                                    + workbook.Name + "/" + workbook.Id + ", "
                                    + "expected user: "******", "
                                    + "actual user: " + updatedWorkbook.OwnerId
                                    );
        }

        return(true);
    }
예제 #15
0
    /// <summary>
    /// Assign ownership
    /// </summary>
    /// <param name="workbook"></param>
    /// <param name="publishSettings"></param>
    /// <param name="siteUsers"></param>
    /// <returns>TRUE: The server content has the correct owner now.  FALSE: We were unable to give the server content the correct owner</returns>
    private bool AttemptOwnerReassignment(SiteWorkbook workbook, WorkbookPublishSettings publishSettings, IEnumerable<SiteUser> siteUsers)
    {
        this.StatusLog.AddStatusHeader("Attempting ownership assignement for Workbook " + workbook.Name + "/" + workbook.Id);

        //Something went wrong if we don't have a set of site users to do the look up
        if (siteUsers == null)
        {
            throw new ArgumentException("No set of site users provided for lookup");
        }

        //Look the local meta data to see what the desired name is
        var desiredOwnerName = publishSettings.OwnerName;
        if(string.IsNullOrEmpty(desiredOwnerName))
        {
            this.StatusLog.AddStatus("Skipping owner assignment. The local file system has no metadata with an owner information for " + workbook.Name);
            LogManualAction_ReassignOwnership(workbook.Name, "none specified", "No client ownership information was specified");
            return true; //Since there is no ownership preference stated locally, then ownership we assigned during upload was fine.
        }

        //Look on the list of users in the target site/server, and try to find a match
        //
        //NOTE: We are doing a CASE INSENSITIVE name comparison. We assume that there are not 2 users with the same name on server w/differet cases
        //      Because if this, we want to be flexible and allow that our source/destination servers may have the user name specified with a differnt
        //      case.  -- This is less secure than a case-sensitive comparison, but is almost always what we want when porting content between servers
        var desiredServerUser = SiteUser.FindUserWithName(siteUsers, desiredOwnerName, StringComparison.InvariantCultureIgnoreCase);

        if(desiredServerUser == null)
        {
            this.StatusLog.AddError("The local file has a workbook/user mapping: " + workbook.Name + "/" + desiredOwnerName + ", but this user does not exist on the target site");
            LogManualAction_ReassignOwnership(workbook.Name, desiredOwnerName, "The target site does not contain a user name that matches the owner specified by the local metadata");
            return false; //Not a run time error, but we have manual steps to perform
        }

        //If the server content is already correct, then there is nothing to do
        if(desiredServerUser.Id == workbook.OwnerId)
        {
            this.StatusLog.AddStatus("Workbook " + workbook.Name + "/" + workbook.Id + ", already has correct ownership. No update requried");
            return true;
        }

        //Lets tell server to update the owner
        var changeOwnership = new SendUpdateWorkbookOwner(_onlineUrls, _onlineSession, workbook.Id, desiredServerUser.Id);
        SiteWorkbook updatedWorkbook;
        try
        {
            this.StatusLog.AddStatus("Server request to change Workbook ownership, wb: " + workbook.Name + "/" + workbook.Id + ", user:"******"/" + desiredServerUser.Id);
            updatedWorkbook = changeOwnership.ExecuteRequest();
        }
        catch (Exception exChangeOnwnerhsip)
        {
            throw exChangeOnwnerhsip; //Unexpected error, send it upward
        }

        //Sanity check the result we got back: Double check to make sure we have the expected owner.
        if(updatedWorkbook.OwnerId != desiredServerUser.Id)
        {
            this.StatusLog.AddError("Unexpected server error! Updated workbook Owner Id does not match expected. wb: "
                + workbook.Name + "/" + workbook.Id + ", "
                + "expected user: "******", "
                + "actual user: " + updatedWorkbook.OwnerId
                );
        }

        return true;
    }
 /// <summary>
 /// Constructor
 /// </summary>
 /// <param name="onlineUrls">Tableau Server Information</param>
 /// <param name="logInInfo">Tableau Sign In Information</param>
 /// <param name="workbook">IEnumerable of SiteWorkbook objects</param>
 /// <param name="localSavePath">Local path where the workbooks should be saved</param>
 public DownloadWorkbook(TableauServerUrls onlineUrls, TableauServerSignIn logInInfo, SiteWorkbook workbook, string localSavePath)
     : base(logInInfo)
 {
     _onlineUrls    = onlineUrls;
     _workbook      = workbook;
     _localSavePath = localSavePath;
 }
    /// <summary>
    /// Update the owner of a single workbook
    /// </summary>
    /// <param name="siteSignIn"></param>
    /// <param name="contentItem"></param>
    /// <param name="userOldOwner"></param>
    /// <param name="userNewOwner"></param>
    private void Execute_ProvisionOwnership_SingleUserChange_SingleWorkbook_inner(TableauServerSignIn siteSignIn, SiteWorkbook contentItem, SiteUser userOldOwner, SiteUser userNewOwner)
    {
        var updateContentOwner = new SendUpdateWorkbookOwner(siteSignIn, contentItem.Id, userNewOwner.Id);

        updateContentOwner.ExecuteRequest();

        CSVRecord_ContentOwnershipModified("workbook", contentItem.Name, userOldOwner.Name, userNewOwner.Name);
    }
예제 #18
0
    /// <summary>
    /// Add data source connection data
    /// </summary>
    /// <param name="thisWorkbook"></param>
    private void AddWorkbookConnectionData(SiteWorkbook thisWorkbook)
    {
        var dataConnections = thisWorkbook.DataConnections;
        if(dataConnections == null)
        {
        return;
        }

        //Write out details for each data connection
        foreach (var thisConnection in dataConnections)
        {
        this.AddKeyValuePairs(
            new string[] {
                ContentType               //1
                ,ContentId                //2
                ,ContentConnectionType    //3
                ,ContentConnectionServer  //4
                ,ContentConnectionPort    //5
                ,ContentConnectionUserName//6
                ,ContentWorkbookId        //7
                ,ContentWorkbookName      //8
                ,ContentProjectId         //9
                ,ContentProjectName       //10
                ,ContentOwnerId           //11
                ,DeveloperNotes           //12
                        },
            new string[] {
                "data-connection"              //1
                ,thisConnection.Id             //2
                ,thisConnection.ConnectionType //3
                ,thisConnection.ServerAddress  //4
                ,thisConnection.ServerPort     //5
                ,thisConnection.UserName       //6
                ,thisWorkbook.Id               //7
                ,thisWorkbook.Name             //8
                ,thisWorkbook.ProjectId        //9
                ,thisWorkbook.ProjectName      //10
                ,thisWorkbook.OwnerId          //11
                ,thisWorkbook.DeveloperNotes   //12
                        });
        }
    }
    /// <summary>
    /// Change the ownership for a single Workbook
    /// </summary>
    /// <param name="contentItem"></param>
    /// <param name="userOldOwner"></param>
    /// <param name="userNewOwner"></param>
    private void Execute_ProvisionOwnership_SingleUserChange_SingleWorkbook(TableauServerSignIn siteSignIn, SiteWorkbook contentItem, SiteUser userOldOwner, SiteUser userNewOwner)
    {
        try
        {
            Execute_ProvisionOwnership_SingleUserChange_SingleWorkbook_inner(siteSignIn, contentItem, userOldOwner, userNewOwner);
        }
        catch (Exception ex)
        {
            _statusLogs.AddError("Error attempting to change content ownership, "
                                 + "workbook: " + contentItem.Name
                                 + "from: " + userOldOwner.Name
                                 + ", to:" + userNewOwner.Name
                                 + ", error: " + ex.ToString());

            CSVRecord_ErrorUpdatingContentOwnership("workbook", contentItem.Name, userOldOwner.Name, userNewOwner.Name, ex.Message);
        }
    }