/// <summary> Get list of book numbers in PT project. </summary> public IReadOnlyList <int> GetBookList(UserSecret userSecret, string ptProjectId) { ScrText scrText = ScrTextCollection.FindById(GetParatextUsername(userSecret), ptProjectId); if (scrText == null) { return(Array.Empty <int>()); } return(scrText.Settings.BooksPresentSet.SelectedBookNumbers.ToArray()); }
/// <summary> Get PT book text in USX, or throw if can't. </summary> public string GetBookText(UserSecret userSecret, string ptProjectId, int bookNum) { ScrText scrText = ScrTextCollection.FindById(GetParatextUsername(userSecret), ptProjectId); if (scrText == null) { throw new DataNotFoundException("Can't get access to cloned project."); } string usfm = scrText.GetText(bookNum); return(UsfmToUsx.ConvertToXmlString(scrText, bookNum, usfm, false)); }
/// <summary> Get notes from the Paratext project folder. </summary> public string GetNotes(UserSecret userSecret, string projectId, int bookNum) { // TODO: should return some data structure instead of XML ScrText scrText = ScrTextCollection.FindById(GetParatextUsername(userSecret), projectId); if (scrText == null) { return(null); } CommentManager manager = CommentManager.Get(scrText); var threads = manager.FindThreads((commentThread) => { return(commentThread.VerseRef.BookNum == bookNum); }, true); return(NotesFormatter.FormatNotes(threads)); }
/// <summary> /// Ensure the target project repository exists on the local SF server, cloning if necessary. /// </summary> private void EnsureProjectReposExists(UserSecret userSecret, ParatextProject target, IInternetSharedRepositorySource repositorySource) { string username = GetParatextUsername(userSecret); bool targetNeedsCloned = ScrTextCollection.FindById(username, target.ParatextId) == null; if (target is ParatextResource resource) { // If the target is a resource, install it InstallResource(resource, target.ParatextId, targetNeedsCloned); } else if (targetNeedsCloned) { SharedRepository targetRepo = new SharedRepository(target.ShortName, HexId.FromStr(target.ParatextId), RepositoryType.Shared); CloneProjectRepo(repositorySource, target.ParatextId, targetRepo); } }
/// <summary> Write up-to-date notes from the mongo database to the Paratext project folder </summary> public void PutNotes(UserSecret userSecret, string projectId, string notesText) { // TODO: should accept some data structure instead of XML string username = GetParatextUsername(userSecret); List <string> users = new List <string>(); int nbrAddedComments = 0, nbrDeletedComments = 0, nbrUpdatedComments = 0; ScrText scrText = ScrTextCollection.FindById(username, projectId); if (scrText == null) { throw new DataNotFoundException("Can't get access to cloned project."); } CommentManager manager = CommentManager.Get(scrText); var ptUser = new SFParatextUser(username); var notes = NotesFormatter.ParseNotes(notesText, ptUser); // Algorithm sourced from Paratext DataAccessServer foreach (var thread in notes) { CommentThread existingThread = manager.FindThread(thread[0].Thread); foreach (var comment in thread) { var existingComment = existingThread?.Comments.FirstOrDefault(c => c.Id == comment.Id); if (existingComment == null) { manager.AddComment(comment); nbrAddedComments++; } else if (comment.Deleted) { existingComment.Deleted = true; nbrDeletedComments++; } else { existingComment.ExternalUser = comment.ExternalUser; existingComment.Contents = comment.Contents; existingComment.VersionNumber += 1; nbrUpdatedComments++; } if (!users.Contains(comment.User)) { users.Add(comment.User); } } } try { foreach (string user in users) { manager.SaveUser(user, false); } _paratextDataHelper.CommitVersionedText(scrText, $"{nbrAddedComments} notes added and " + $"{nbrDeletedComments + nbrUpdatedComments} notes updated or deleted in synchronize"); _logger.LogInformation("{0} added {1} notes, updated {2} notes and deleted {3} notes", userSecret.Id, nbrAddedComments, nbrUpdatedComments, nbrDeletedComments); } catch (Exception e) { _logger.LogError(e, "Exception while updating notes: {0}", e.Message); } }
/// <summary> Write up-to-date book text from mongo database to Paratext project folder. </summary> public async Task PutBookText(UserSecret userSecret, string projectId, int bookNum, string usx, Dictionary <int, string> chapterAuthors = null) { string username = GetParatextUsername(userSecret); ScrText scrText = ScrTextCollection.FindById(username, projectId); var doc = new XmlDocument { PreserveWhitespace = true }; doc.LoadXml(usx); UsxFragmenter.FindFragments(scrText.ScrStylesheet(bookNum), doc.CreateNavigator(), XPathExpression.Compile("*[false()]"), out string usfm); usfm = UsfmToken.NormalizeUsfm(scrText.ScrStylesheet(bookNum), usfm, false, scrText.RightToLeft, scrText); if (chapterAuthors == null || chapterAuthors.Count == 0) { // If we don't have chapter authors, update book as current user if (scrText.Permissions.AmAdministrator) { // if the current user is an administrator, then always allow editing the book text even if the user // doesn't have permission. This will ensure that a sync by an administrator never fails. scrText.Permissions.RunWithEditPermision(bookNum, () => scrText.PutText(bookNum, 0, false, usfm, null)); } else { scrText.PutText(bookNum, 0, false, usfm, null); } _logger.LogInformation("{0} updated {1} in {2}.", userSecret.Id, Canon.BookNumberToEnglishName(bookNum), scrText.Name); } else { // As we have a list of chapter authors, build a dictionary of ScrTexts for each of them Dictionary <string, ScrText> scrTexts = new Dictionary <string, ScrText>(); foreach (string userId in chapterAuthors.Values.Distinct()) { if (userId == userSecret.Id) { scrTexts.Add(userId, scrText); } else { // Get their user secret, so we can get their username, and create their ScrText UserSecret authorUserSecret = await _userSecretRepository.GetAsync(userId); string authorUserName = GetParatextUsername(authorUserSecret); scrTexts.Add(userId, ScrTextCollection.FindById(authorUserName, projectId)); } } // If there is only one author, just write the book if (scrTexts.Count == 1) { scrTexts.Values.First().PutText(bookNum, 0, false, usfm, null); _logger.LogInformation("{0} updated {1} in {2}.", scrTexts.Keys.First(), Canon.BookNumberToEnglishName(bookNum), scrText.Name); } else { // Split the usfm into chapters List <string> chapters = ScrText.SplitIntoChapters(scrText.Name, bookNum, usfm); // Put the individual chapters foreach ((int chapterNum, string authorUserId) in chapterAuthors) { if ((chapterNum - 1) < chapters.Count) { // The ScrText permissions will be the same as the last sync's permissions, so no need to check scrTexts[authorUserId].PutText(bookNum, chapterNum, false, chapters[chapterNum - 1], null); _logger.LogInformation("{0} updated chapter {1} of {2} in {3}.", authorUserId, chapterNum, Canon.BookNumberToEnglishName(bookNum), scrText.Name); } } } } }
/// <summary> Determine if a specific project is in a right to left language. </summary> public bool IsProjectLanguageRightToLeft(UserSecret userSecret, string ptProjectId) { ScrText scrText = ScrTextCollection.FindById(GetParatextUsername(userSecret), ptProjectId); return(scrText == null ? false : scrText.RightToLeft); }
/// <summary> /// Gets the permissions for a project or resource. /// </summary> /// <param name="userSecret">The user secret.</param> /// <param name="project">The project - the UserRoles and ParatextId are used.</param> /// <param name="ptUsernameMapping">A mapping of user ID to Paratext username.</param> /// <param name="book">The book number. Set to zero to check for all books.</param> /// <param name="chapter">The chapter number. Set to zero to check for all books.</param> /// <returns> /// A dictionary of permissions where the key is the user ID and the value is the permission. /// </returns> /// <remarks> /// See <see cref="TextInfoPermission" /> for permission values. /// A dictionary is returned, as permissions can be updated. /// </remarks> public async Task <Dictionary <string, string> > GetPermissionsAsync(UserSecret userSecret, SFProject project, IReadOnlyDictionary <string, string> ptUsernameMapping, int book = 0, int chapter = 0) { var permissions = new Dictionary <string, string>(); // See if the source is a resource if (project.ParatextId.Length == SFInstallableDblResource.ResourceIdentifierLength) { foreach (string uid in project.UserRoles.Keys) { permissions.Add(uid, await this.GetResourcePermissionAsync(project.ParatextId, uid)); } } else { // Get the scripture text so we can retrieve the permissions from the XML ScrText scrText = ScrTextCollection.FindById(GetParatextUsername(userSecret), project.ParatextId); // Calculate the project and resource permissions foreach (string uid in project.UserRoles.Keys) { // See if the user is in the project members list if (!ptUsernameMapping.TryGetValue(uid, out string userName) || string.IsNullOrWhiteSpace(userName) || scrText.Permissions.GetRole(userName) == Paratext.Data.Users.UserRoles.None) { permissions.Add(uid, TextInfoPermission.None); } else { string textInfoPermission = TextInfoPermission.Read; if (book == 0) { // Project level if (scrText.Permissions.CanEditAllBooks(userName)) { textInfoPermission = TextInfoPermission.Write; } } else if (chapter == 0) { // Book level IEnumerable <int> editable = scrText.Permissions.GetEditableBooks( Paratext.Data.Users.PermissionSet.Merged, userName); if (editable == null || !editable.Any()) { // If there are no editable book permissions, check if they can edit all books if (scrText.Permissions.CanEditAllBooks(userName)) { textInfoPermission = TextInfoPermission.Write; } } else if (editable.Contains(book)) { textInfoPermission = TextInfoPermission.Write; } } else { // Chapter level IEnumerable <int> editable = scrText.Permissions.GetEditableChapters(book, scrText.Settings.Versification, userName, Paratext.Data.Users.PermissionSet.Merged); if (editable?.Contains(chapter) ?? false) { textInfoPermission = TextInfoPermission.Write; } } permissions.Add(uid, textInfoPermission); } } } return(permissions); }
/// <summary> /// Synchronizes the text and notes data on the SF server with the data on the Paratext server. /// </summary> public async Task SendReceiveAsync(UserSecret userSecret, string ptTargetId, IProgress <ProgressState> progress = null) { if (userSecret == null || ptTargetId == null) { throw new ArgumentNullException(); } IInternetSharedRepositorySource source = await GetInternetSharedRepositorySource(userSecret.Id); IEnumerable <SharedRepository> repositories = source.GetRepositories(); IEnumerable <ProjectMetadata> projectsMetadata = source.GetProjectsMetaData(); IEnumerable <string> projectGuids = projectsMetadata.Select(pmd => pmd.ProjectGuid.Id); Dictionary <string, ParatextProject> ptProjectsAvailable = GetProjects(userSecret, repositories, projectsMetadata).ToDictionary(ptProject => ptProject.ParatextId); if (!projectGuids.Contains(ptTargetId)) { // See if this is a resource IReadOnlyList <ParatextResource> resources = await this.GetResourcesInternalAsync(userSecret.Id, true); ParatextResource resource = resources.SingleOrDefault(r => r.ParatextId == ptTargetId); if (resource != null) { ptProjectsAvailable.Add(resource.ParatextId, resource); } else { _logger.LogWarning($"The target project did not have a full name available {ptTargetId}"); } } if (!ptProjectsAvailable.TryGetValue(ptTargetId, out ParatextProject targetPtProject)) { throw new ArgumentException( $"PT projects with the following PT ids were requested but without access or they don't exist: {ptTargetId}"); } EnsureProjectReposExists(userSecret, targetPtProject, source); StartProgressReporting(progress); if (!(targetPtProject is ParatextResource)) { SharedProject targetSharedProj = SharingLogicWrapper.CreateSharedProject(ptTargetId, targetPtProject.ShortName, source.AsInternetSharedRepositorySource(), repositories); string username = GetParatextUsername(userSecret); // Specifically set the ScrText property of the SharedProject to indicate the project is available locally targetSharedProj.ScrText = ScrTextCollection.FindById(username, ptTargetId); targetSharedProj.Permissions = targetSharedProj.ScrText.Permissions; List <SharedProject> sharedPtProjectsToSr = new List <SharedProject> { targetSharedProj }; // TODO report results List <SendReceiveResult> results = Enumerable.Empty <SendReceiveResult>().ToList(); bool success = false; bool noErrors = SharingLogicWrapper.HandleErrors(() => success = SharingLogicWrapper .ShareChanges(sharedPtProjectsToSr, source.AsInternetSharedRepositorySource(), out results, sharedPtProjectsToSr)); if (!noErrors || !success) { throw new InvalidOperationException( "Failed: Errors occurred while performing the sync with the Paratext Server."); } } }