public void HandleCheckInCurrentBook(ApiRequest request) { Action <float> reportCheckinProgress = (fraction) => { dynamic messageBundle = new DynamicJson(); messageBundle.fraction = fraction; _socketServer.SendBundle("checkinProgress", "progress", messageBundle); // The status panel is supposed to be showing a progress bar in response to getting the bundle, // but since we're doing the checkin on the UI thread, it doesn't get painted without this. Application.DoEvents(); }; try { // Right before calling this API, the status panel makes a change that // should make the progress bar visible. But this method is running on // the UI thread so without this call it won't appear until later, when // we have Application.DoEvents() as part of reporting progress. We do // quite a bit on large books before the first file is written to the // zip, so one more DoEvents() here lets the bar appear at once. Application.DoEvents(); _bookSelection.CurrentSelection.Save(); if (!_tcManager.CheckConnection()) { request.Failed(); return; } var bookName = Path.GetFileName(_bookSelection.CurrentSelection.FolderPath); if (_tcManager.CurrentCollection.OkToCheckIn(bookName)) { // review: not super happy about this being here in the api. Was stymied by // PutBook not knowing about the actual book object, but maybe that could be passed in. // It's important that this is done BEFORE the checkin: we want other users to see the // comment, and NOT see the pending comment as if it was their own if they check out. var message = BookHistory.GetPendingCheckinMessage(_bookSelection.CurrentSelection); BookHistory.AddEvent(_bookSelection.CurrentSelection, BookHistoryEventType.CheckIn, message); BookHistory.SetPendingCheckinMessage(_bookSelection.CurrentSelection, ""); _tcManager.CurrentCollection.PutBook(_bookSelection.CurrentSelection.FolderPath, true, false, reportCheckinProgress); reportCheckinProgress(0); // hides the progress bar (important if a different book has been selected that is still checked out) Analytics.Track("TeamCollectionCheckinBook", new Dictionary <string, string>() { { "CollectionId", _settings?.CollectionId }, { "CollectionName", _settings?.CollectionName }, { "Backend", _tcManager?.CurrentCollection?.GetBackendType() }, { "User", CurrentUser }, { "BookId", _bookSelection?.CurrentSelection.ID }, { "BookName", _bookSelection?.CurrentSelection.Title } }); } else { // We can't check in! The system has broken down...perhaps conflicting checkouts while offline. // Save our version in Lost-and-Found _tcManager.CurrentCollection.PutBook(_bookSelection.CurrentSelection.FolderPath, false, true, reportCheckinProgress); reportCheckinProgress(0); // cleans up panel for next time // overwrite it with the current repo version. _tcManager.CurrentCollection.CopyBookFromRepoToLocal(bookName, dialogOnError: true); // Force a full reload of the book from disk and update the UI to match. _bookSelection.SelectBook(_bookServer.GetBookFromBookInfo(_bookSelection.CurrentSelection.BookInfo, true)); var msg = LocalizationManager.GetString("TeamCollection.ConflictingEditOrCheckout", "Someone else has edited this book or checked it out even though you were editing it! Your changes have been saved to Lost and Found"); ErrorReport.NotifyUserOfProblem(msg); Analytics.Track("TeamCollectionConflictingEditOrCheckout", new Dictionary <string, string>() { { "CollectionId", _settings?.CollectionId }, { "CollectionName", _settings?.CollectionName }, { "Backend", _tcManager?.CurrentCollection?.GetBackendType() }, { "User", CurrentUser }, { "BookId", _bookSelection?.CurrentSelection?.ID }, { "BookName", _bookSelection?.CurrentSelection?.Title } }); } UpdateUiForBook(); request.PostSucceeded(); Application.Idle += OnIdleConnectionCheck; } catch (Exception e) { reportCheckinProgress(0); // cleans up panel progress indicator var msgId = "TeamCollection.ErrorCheckingBookIn"; var msgEnglish = "Error checking in {0}: {1}"; var log = _tcManager?.CurrentCollection?.MessageLog; // Pushing an error into the log will show the Reload Collection button. It's not obvious this // is useful here, since we don't know exactly what went wrong. However, it at least gives the user // the option to try it. if (log != null) { log.WriteMessage(MessageAndMilestoneType.Error, msgId, msgEnglish, _bookSelection?.CurrentSelection?.FolderPath, e.Message); } Logger.WriteError(String.Format(msgEnglish, _bookSelection?.CurrentSelection?.FolderPath, e.Message), e); NonFatalProblem.ReportSentryOnly(e, $"Something went wrong for {request.LocalPath()} ({_bookSelection?.CurrentSelection?.FolderPath})"); request.Failed("checkin failed"); } }
// Needs to be thread-safe private string GetBookStatusJson(string bookFolderName, Book.Book book) { string whoHasBookLocked = null; DateTime whenLocked = DateTime.MaxValue; bool problem = false; // bookFolderName may be null when no book is selected, e.g., after deleting one. var status = bookFolderName == null ? null :_tcManager.CurrentCollection?.GetStatus(bookFolderName); // At this level, we know this is the path to the .bloom file in the repo // (though if we implement another backend, we'll have to generalize the notion somehow). // For the Javascript, it's just an argument to pass to // CommonMessages.GetPleaseClickHereForHelpMessage(). It's only used if hasInvalidRepoData is non-empty. string clickHereArg = ""; var folderTC = _tcManager.CurrentCollection as FolderTeamCollection; if (folderTC != null && bookFolderName != null) { clickHereArg = UrlPathString.CreateFromUnencodedString(folderTC.GetPathToBookFileInRepo(bookFolderName)) .UrlEncoded; } string hasInvalidRepoData = (status?.hasInvalidRepoData ?? false) ? (folderTC)?.GetCouldNotOpenCorruptZipMessage() : ""; if (bookFolderName == null) { return(JsonConvert.SerializeObject( new { // Keep this in sync with IBookTeamCollectionStatus defined in TeamCollectionApi.tsx who = "", whoFirstName = "", whoSurname = "", when = DateTime.Now.ToShortDateString(), where = "", currentUser = CurrentUser, currentUserName = TeamCollectionManager.CurrentUserFirstName, currentMachine = TeamCollectionManager.CurrentMachine, problem = "", hasInvalidRepoData = false, clickHereArg = "", changedRemotely = false, disconnected = false, newLocalBook = true, checkinMessage = "", isUserAdmin = _tcManager.OkToEditCollectionSettings })); } bool newLocalBook = false; try { whoHasBookLocked = _tcManager.CurrentCollectionEvenIfDisconnected?.WhoHasBookLocked(bookFolderName); // It's debatable whether to use CurrentCollectionEvenIfDisconnected everywhere. For now, I've only changed // it for the two bits of information actually needed by the status panel when disconnected. whenLocked = _tcManager.CurrentCollection?.WhenWasBookLocked(bookFolderName) ?? DateTime.MaxValue; if (whoHasBookLocked == TeamCollection.FakeUserIndicatingNewBook) { // This situation comes about from two different scenarios: // 1) The user is creating a new book and TeamCollection status doesn't matter // 2) The user is trying to check out an existing book and TeamCollectionManager // discovers [through CheckConnection()] that it is suddenly in a disconnected // state. // In both cases, the current selected book is in view. The only way to tell // these two situations apart is that in (1) book.IsSaveable is true // and in (2) it is not. // Or, book may be null because we're just getting a status to show in the list // of all books. In that case, book is null, but it's fairly safe to assume it's a new local book. if (book?.IsSaveable ?? true) { whoHasBookLocked = CurrentUser; newLocalBook = true; } else { whoHasBookLocked = null; } } problem = _tcManager.CurrentCollection?.HasLocalChangesThatMustBeClobbered(bookFolderName) ?? false; } catch (Exception e) when(e is ICSharpCode.SharpZipLib.Zip.ZipException || e is IOException) { hasInvalidRepoData = (_tcManager.CurrentCollection as FolderTeamCollection)?.GetCouldNotOpenCorruptZipMessage(); } // If the request asked for the book by name, we don't have an actual Book object. // However, it happens that those requests don't need the checkinMessage. var checkinMessage = book == null ? "" : BookHistory.GetPendingCheckinMessage(book); return(JsonConvert.SerializeObject( new { // Keep this in sync with IBookTeamCollectionStatus defined in TeamCollectionApi.tsx who = whoHasBookLocked, whoFirstName = _tcManager.CurrentCollection?.WhoHasBookLockedFirstName(bookFolderName), whoSurname = _tcManager.CurrentCollection?.WhoHasBookLockedSurname(bookFolderName), when = whenLocked.ToLocalTime().ToShortDateString(), where = _tcManager.CurrentCollectionEvenIfDisconnected?.WhatComputerHasBookLocked(bookFolderName), currentUser = CurrentUser, currentUserName = TeamCollectionManager.CurrentUserFirstName, currentMachine = TeamCollectionManager.CurrentMachine, problem, hasInvalidRepoData, clickHereArg, changedRemotely = _tcManager.CurrentCollection?.HasBeenChangedRemotely(bookFolderName), disconnected = _tcManager.CurrentCollectionEvenIfDisconnected?.IsDisconnected, newLocalBook, checkinMessage, isUserAdmin = _tcManager.OkToEditCollectionSettings })); }