Repository: oppia/oppia

Issue:
Error on opening existing explorations on the test server
**Describe the bug**
On opening existing explorations on the test server, an error is obtained which says 
![image](https://user-images.githubusercontent.com/11898234/65528126-744aad80-df11-11e9-99a6-680c48a6ef7d.png)

(The exploration is [here](https://oppiatestserver.appspot.com/create/c0xv8d3Gt1A1#/improvements))

/cc @brianrodri looks serious? I tried on multiple existing explorations and it failed.

**To Reproduce**
Steps to reproduce the behavior:
 1. Go to [this exploration](https://oppiatestserver.appspot.com/create/c0xv8d3Gt1A1#/improvements)
 2. See the error displayed

**Expected behavior**
There should be no error messages.

**Desktop (please complete the following information; delete this section if the issue does not arise on desktop):**
 - OS: OS X
 - Browser Chrome
 - Version 77

Warning message visible to guest users on visiting exploration editor page
**Steps to reproduce:**
- Log out from the [test server](https://oppiatestserver.appspot.com/).
- Visit any [exploration editor page](https://oppiatestserver.appspot.com/create/6bzU6wNnYqby#/gui/Introduction).

**Error log:**
`You must be logged in to access this resource.`

**Expected behaviour**:
There shouldn't be any such error log on editor page for guest users.

**Screenshot:**

![image](https://user-images.githubusercontent.com/16653571/65941047-ac894900-e447-11e9-85da-d597c86600b7.png)

/cc @anubhavsinha98 , is it because of the "solicit answer" handlers? 



Discussion:
@brianrodri could you PTAL at this? This is blocking the release of the improvements tab.
I tried reproducing this and I saw another error in the console due to missing require statement for capitalize filter. I am not sure if this issue is related to that but I have issued #7757 to fix the capitalize filter.

Here is a screenshot of the error I get:

<img width="1643" alt="Screenshot 2019-10-09 at 11 11 58 PM" src="https://user-images.githubusercontent.com/15226041/66506471-05fa1380-eaeb-11e9-820c-334fd4246df7.png">

Thanks!
I added some brief analysis in the doc [here](https://docs.google.com/document/d/1VarxAJNuzQA2FEKtvstbHT2mXLt2l7ngnT45Gfc5j-4/edit?usp=sharing).

@nithusha21 @DubeySandeep can you suggest on what can we do next? Do we need to add more logging info to debug this issue since the source of error is not clear?
Hi @kevintab95, can you please look into [this](https://docs.google.com/document/d/1VarxAJNuzQA2FEKtvstbHT2mXLt2l7ngnT45Gfc5j-4/edit?usp=sharing) analysis once if you are free and provide some inputs? Thanks!
@DubeySandeep further looked into this and checked the ExpIssuesModel on test server. The model is missing for some of the exp versions of old explorations. We will need to:

- Find a reason for how this happened and improve the saving pipeline for the models
- Fix this issue for the release by finding a way to regenerate these models.

@nithusha21 can you help us with the regeneration process since you have worked on similar pipeline earlier?

Thanks!
I looked into this issue and found the error is happening around this line:
https://github.com/oppia/oppia/blob/0eb964ebfe2f9446d24a2a60e76dcea11c2264c5/core/controllers/editor.py#L534-L538

**Reason:**
On further investigation, I found that the ExplorationIssueModel is missing for the updated version of exploration. I'm not sure how and why the model is missing but I saw a pattern that can cause this issue. In _svae_exploration method, we updated the exploration and connected model but the process is bit buggy. We follow the following pattern:
1. Update the exploration model instance.
2. Commit exploration model instance into datastore.
3. Update connected model instance (like stats model summary mode etc.)
4. Save connected models into datastore one at a time!

So even if by any chance the stats model updation failed the exploration will be in the updated stage. Although I'm not sure whether this is the cause of the issue but I think we should fix it.

**Long term solution:**
We should do the model update in the following steps: 1 --> 3 --> (2, 4) in one call.

**Quick solution to escalating the release process:**

I'm not sure what should we do as we don't know the actual cause of this issue, I think finding the reals cause will help us find the quick solution.

@nithusha21, it would be great if you can help us find the cause of this issue, asking you as you have worked on the stats related model recently and maybe you know what can cause this.   
   

Hi, I took a look at this and I am actually quite confused. So there are many exploration versions for which ExplorationIssuesModel is missing, for example, the exploration https://oppiatestserver.appspot.com/create/Kr_LjJHJ3Fu2 which Ankita mentions, has versions upto 7 but has issue models only for versions 5 and 6. Is it necessary for every exploration version to contain an issue model? If so what is missing in the complete flow of issue model creation which causes such a failure. 

I can confirm that the error is raised at line 538 which Sandeep mentions above (which is hit due to the missing model). @brianrodri @pranavsid98 could you PTAL and let us know what the expected behavior is?
Hi @brianrodri @pranavsid98 can you please help us with this issue since solving this will unblock the release. Thanks!
Hi @DubeySandeep, can you explain what should be the expected behavior? Thanks.
Hi@kevintsb95, I've updated the issue with more details, let me know if you need more information. Thanks!
Hi @DubeySandeep @anubhavsinha98,

I checked this issue and the error comes due to [can_edit_entity](https://github.com/oppia/oppia/blob/develop/core/controllers/acl_decorators.py#L2571) decorator being used in [LearnerAnswerInfoHandler](https://github.com/oppia/oppia/blob/develop/core/controllers/editor.py#L784). 

I am not a lot familiar with this framework and I want to know why is this check required before fetching the answer info? 

We do not have any such test before fetching info for other ImprovementTasksFactory.

Thanks!
> why is this check required before fetching the answer info?

 `can_edit_entity` is used in handlers which we only allow editors to use, I'm not sure what's the expected behavior for the `LearnerAnswerInfoHandler`. Whatever be the case there should not be any warning message shown to a guest user without any action.

If the expected behavior is not to show the `LearnerAnswerInfoHandler` to guest users then don't make such a call if the user isn't logged in.

Note: The Guest user has read permission to all the parts of the editor page once it's published, considering this I think we should allow guest users to read the LearnerAnswerInfoHandler (not sure!).

@anubhavsinha98, can you please update this thread with the intended expected behavior? 
Also, is this blocking? I think we can unprioritize this for the current release and move it to the October release since this will require Anubhav's input.

If we are marking this as blocking, I think we will need to remove the `can_edit_entity` decorator from the `LearnerAnswerInfoHandler`.

What do you think @DubeySandeep?
Hi @DubeySandeep, I looked into this further and I feel we are using a wrong decorator here. Shouldn't we be using `can_view_exploration_stats` decorator instead?
As I've stated this in my above comment:
> Note: The Guest users have read permission to all the parts of the editor page once it's published, considering this I think we should allow guest users to read the LearnerAnswerInfoHandler (not sure!).

I just wanted to have a confirmation on this from @anubhavsinha98, I think we can go ahead and change the decorator for the `GET` request of `LearnerAnswerInfoHandler` to `can_view_exploration_stats`.  
I also see `can_view_exploration_stats` for a post request of `ResolveIssueHandler`: 

https://github.com/oppia/oppia/blob/0eb964ebfe2f9446d24a2a60e76dcea11c2264c5/core/controllers/editor.py#L563-L573

@brianrodri & @anubhavsinha98, is this an expected behavior? 
> Hi @DubeySandeep @anubhavsinha98,
> 
> I checked this issue and the error comes due to [can_edit_entity](https://github.com/oppia/oppia/blob/develop/core/controllers/acl_decorators.py#L2571) decorator being used in [LearnerAnswerInfoHandler](https://github.com/oppia/oppia/blob/develop/core/controllers/editor.py#L784).
> 
> I am not a lot familiar with this framework and I want to know why is this check required before fetching the answer info?
> 
> We do not have any such test before fetching info for other ImprovementTasksFactory.
> 
> Thanks!

Actually, ```can_edit_entity``` checks whether that entity can be edited by that user or not.
It is used when the editor fetches the answer details of exploration, it checks whether the user has the right to edit the entity or not.
> > why is this check required before fetching the answer info?
> 
> `can_edit_entity` is used in handlers which we only allow editors to use, I'm not sure what's the expected behavior for the `LearnerAnswerInfoHandler`. Whatever be the case there should not be any warning message shown to a guest user without any action.
> 
> If the expected behavior is not to show the `LearnerAnswerInfoHandler` to guest users then don't make such a call if the user isn't logged in.
> 
> Note: The Guest user has read permission to all the parts of the editor page once it's published, considering this I think we should allow guest users to read the LearnerAnswerInfoHandler (not sure!).
> 
> @anubhavsinha98, can you please update this thread with the intended expected behavior?

Actually, the Learner answer info in the improvements tab must only be shown only when the editor has logged in. It should not be shown to the guests.
> Hi @DubeySandeep, I looked into this further and I feel we are using a wrong decorator here. Shouldn't we be using `can_view_exploration_stats` decorator instead?

Using ```can_view_exploration_stats``` will remove the notion of making it generalize for all the entities and right now entities stand here for exploration or question. 
Hi @anubhavsinha98 @DubeySandeep would it be a good idea to add a new decorator which works similar to `can_edit_entity` handler but does not raise an Exception if user is not logged in. Instead the get function will check if user is logged in or not and return empty data if user is not logged in? Thanks!
I have issued #7763 with a fix as follows:

I have added a variable `IS_LOGGED_IN_CHECK_REQUIRED` in `EditorHandler` which is set to True. This variable is set to False in `LearnerAnswerInfoHandler`. It is used in the decorator and the editing activity check is skipped if it is False. The `LearnerAnswerInfoHandler` returns empty data now if user is not logged in and no exception is raised.
> Actually, the Learner answer info in the improvements tab must only be shown only when the editor has logged in. It should not be shown to the guests.

@anubhavsinha98, is ther any specific reason for that? Like all other parts of improvements tabs are visible to guest users, right? 
> I have added a variable IS_LOGGED_IN_CHECK_REQUIRED in EditorHandler which is set to True. This variable is set to False in LearnerAnswerInfoHandler. It is used in the decorator and the editing activity check is skipped if it is False. The LearnerAnswerInfoHandler returns empty data now if user is not logged in and no exception is raised.

Instead of changing the decorator let's change the frontend such that it doesn't makes call to the handler if the user deosn't have edit rights. 

Resolving patch:
diff --git a/core/controllers/editor.py b/core/controllers/editor.py
--- a/core/controllers/editor.py
+++ b/core/controllers/editor.py
@@ -567,7 +567,7 @@ class ResolveIssueHandler(EditorHandler):
     instances are deleted.
     """
 
-    @acl_decorators.can_view_exploration_stats
+    @acl_decorators.can_edit_exploration
     def post(self, exp_id):
         """Handles POST requests."""
         exp_issue_dict = self.payload.get('exp_issue_dict')
@@ -781,7 +781,7 @@ class LearnerAnswerInfoHandler(EditorHandler):
 
     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
 
-    @acl_decorators.can_edit_entity
+    @acl_decorators.can_play_entity
     def get(self, entity_type, entity_id):
         """Handles the GET requests for learner answer info for an
         exploration state.


Test patch:
diff --git a/core/tests/protractor_desktop/explorationImprovementsTab.js b/core/tests/protractor_desktop/explorationImprovementsTab.js
--- a/core/tests/protractor_desktop/explorationImprovementsTab.js
+++ b/core/tests/protractor_desktop/explorationImprovementsTab.js
@@ -57,15 +57,12 @@ describe('Answer Details Improvements', function() {
     explorationPlayerPage = new ExplorationPlayerPage.ExplorationPlayerPage();
     libraryPage = new LibraryPage.LibraryPage();
     creatorDashboardPage = new CreatorDashboardPage.CreatorDashboardPage();
-    explorationPlayerPage =
-      new ExplorationPlayerPage.ExplorationPlayerPage();
 
     improvementsTab = explorationEditorPage.getImprovementsTab();
 
-    users.createUser(
-      'learner@user.com', 'learnerUser');
+    users.createUser('learner@ExplorationAnswerDetails.com', 'learnerUser');
     users.createAndLoginAdminUser(
-      'creator@user.com', 'creatorUser');
+      'creator@ExplorationAnswerDetails.com', 'creatorUser');
 
     adminPage.editConfigProperty(
       'Always ask learners for answer details. For testing -- do not use',
@@ -77,6 +74,7 @@ describe('Answer Details Improvements', function() {
 
     // Creator creates and publishes an exploration.
     workflow.createExplorationAsAdmin();
+    general.getExplorationIdFromEditor().then(expId => this.expId = expId);
     explorationEditorMainTab.exitTutorial();
 
     explorationEditorPage.navigateToSettingsTab();
@@ -100,30 +98,46 @@ describe('Answer Details Improvements', function() {
     explorationEditorMainTab.setInteraction('EndExploration');
     explorationEditorPage.saveChanges();
     workflow.publishExploration();
-  });
-
-  it('checks solicit answer details feature', function() {
-    users.login('learner@user.com');
-    libraryPage.get();
-    libraryPage.findExploration(EXPLORATION_TITLE);
-    libraryPage.playExploration(EXPLORATION_TITLE);
-    explorationPlayerPage.submitAnswer('TextInput', 'One');
-    explorationPlayerPage.submitAnswerDetails('I liked this choice of answer');
-    explorationPlayerPage.expectExplorationToNotBeOver();
-
-    oppiaLogo.click();
-    general.acceptAlert();
     users.logout();
-    users.login('creator@user.com');
-    creatorDashboardPage.get();
-    creatorDashboardPage.navigateToExplorationEditor();
-    explorationEditorPage.navigateToImprovementsTab();
+  });
 
-    var task = improvementsTab.getAnswerDetailsTask('One');
-    improvementsTab.clickTaskActionButton(task, 'Review Answer Details');
-    improvementsTab.verifyAnswerDetails('I liked this choi...', 1);
-    improvementsTab.closeModal();
-    users.logout();
+  describe('Solicit answer details', function() {
+    beforeAll(function() {
+      users.login('learner@ExplorationAnswerDetails.com');
+      libraryPage.get();
+      libraryPage.findExploration(EXPLORATION_TITLE);
+      libraryPage.playExploration(EXPLORATION_TITLE);
+      explorationPlayerPage.submitAnswer('TextInput', 'One');
+      explorationPlayerPage.submitAnswerDetails(
+        'I liked this choice of answer');
+      explorationPlayerPage.expectExplorationToNotBeOver();
+      oppiaLogo.click();
+      general.acceptAlert();
+      users.logout();
+    });
+
+    it('is visible for creators', function() {
+      users.login('creator@ExplorationAnswerDetails.com');
+      creatorDashboardPage.get();
+      creatorDashboardPage.navigateToExplorationEditor();
+      explorationEditorPage.navigateToImprovementsTab();
+
+      var task = improvementsTab.getAnswerDetailsTask('One');
+      improvementsTab.clickTaskActionButton(task, 'Review Answer Details');
+      improvementsTab.verifyAnswerDetails('I liked this choi...', 1);
+      improvementsTab.closeModal();
+      users.logout();
+    });
+
+    it('is visible for guests as read-only', function() {
+      general.openEditor(this.expId);
+      explorationEditorPage.navigateToImprovementsTab();
+
+      var task = improvementsTab.getAnswerDetailsTask('One');
+      improvementsTab.clickTaskActionButton(task, 'Review Answer Details');
+      improvementsTab.verifyReadOnlyAnswerDetails('I liked this choi...', 1);
+      improvementsTab.closeModal();
+    });
   });
 
   afterAll(function() {
@@ -229,7 +243,6 @@ describe('Feedback Improvements', function() {
       EXPLORATION_CATEGORY,
       EXPLORATION_OBJECTIVE,
       EXPLORATION_LANGUAGE);
-    creatorDashboardPage.get();
     users.logout();
 
     // Learner plays the exploration and submits a feedback.
@@ -276,7 +289,7 @@ describe('Feedback Improvements', function() {
 });
 
 
-describe('Suggestions on Explorations', function() {
+describe('Suggestions Improvements', function() {
   var EXPLORATION_TITLE = 'Exploration with Suggestion';
   var EXPLORATION_CATEGORY = 'Algorithms';
   var EXPLORATION_OBJECTIVE = 'To explore something new';
@@ -391,3 +404,153 @@ describe('Suggestions on Explorations', function() {
     general.checkForConsoleErrors([]);
   });
 });
+
+describe('Playthrough Improvements', function() {
+  var EXPLORATION_TITLE = 'Exploration with Early Quit Playthroughs';
+  var EXPLORATION_CATEGORY = 'Algorithms';
+  var EXPLORATION_OBJECTIVE = 'To explore something new';
+  var EXPLORATION_LANGUAGE = 'English';
+  var CREATOR_EMAIL = 'creator@ExplorationEarlyQuit.com';
+  var LEARNER_EMAIL = 'learner@ExplorationEarlyQuit.com';
+
+  var adminPage = null;
+  var creatorDashboardPage = null;
+  var explorationEditorPage = null;
+  var explorationEditorMainTab = null;
+  var explorationEditorSettingsTab = null;
+  var explorationPlayerPage = null;
+  var libraryPage = null;
+  var oppiaLogo = $('.protractor-test-oppia-main-logo');
+
+  var improvementsTab = null;
+
+  beforeAll(function() {
+    adminPage = new AdminPage.AdminPage();
+    creatorDashboardPage = new CreatorDashboardPage.CreatorDashboardPage();
+    explorationEditorPage = new ExplorationEditorPage.ExplorationEditorPage();
+    explorationEditorMainTab = explorationEditorPage.getMainTab();
+    explorationEditorSettingsTab = explorationEditorPage.getSettingsTab();
+    explorationPlayerPage = new ExplorationPlayerPage.ExplorationPlayerPage();
+    libraryPage = new LibraryPage.LibraryPage();
+
+    improvementsTab = explorationEditorPage.getImprovementsTab();
+
+    users.createUser(LEARNER_EMAIL, 'learnerEarlyQuit');
+    users.createAndLoginAdminUser(CREATOR_EMAIL, 'creatorEarlyQuit');
+
+    workflow.createExplorationAsAdmin();
+    explorationEditorMainTab.exitTutorial();
+
+    explorationEditorPage.navigateToSettingsTab();
+    explorationEditorSettingsTab.setTitle(EXPLORATION_TITLE);
+    explorationEditorSettingsTab.setCategory(EXPLORATION_CATEGORY);
+    explorationEditorSettingsTab.setObjective(EXPLORATION_OBJECTIVE);
+    explorationEditorSettingsTab.setLanguage(EXPLORATION_LANGUAGE);
+
+    explorationEditorPage.navigateToMainTab();
+    explorationEditorMainTab.setStateName('One');
+    explorationEditorMainTab.setContent(
+      forms.toRichText('Please write 1 in words.'));
+    explorationEditorMainTab.setInteraction('TextInput');
+    explorationEditorMainTab.addResponse(
+      'TextInput', forms.toRichText('Good job'), 'Two', true, 'Equals',
+      'One');
+    explorationEditorMainTab.getResponseEditor('default').setFeedback(
+      forms.toRichText('Try again'));
+
+    explorationEditorMainTab.moveToState('Two');
+    explorationEditorMainTab.setContent(
+      forms.toRichText('Please write 2 in words.'));
+    explorationEditorMainTab.setInteraction('TextInput');
+    explorationEditorMainTab.addResponse(
+      'TextInput', forms.toRichText('Good job'), 'Three', true, 'Equals',
+      'Two');
+    explorationEditorMainTab.getResponseEditor('default').setFeedback(
+      forms.toRichText('Try again'));
+
+    explorationEditorMainTab.moveToState('Three');
+    explorationEditorMainTab.setContent(
+      forms.toRichText('Please write 3 in words.'));
+    explorationEditorMainTab.setInteraction('TextInput');
+    explorationEditorMainTab.addResponse(
+      'TextInput', forms.toRichText('Good job'), 'End', true, 'Equals',
+      'Three');
+    explorationEditorMainTab.addResponse(
+      'TextInput', forms.toRichText('Try 2 again'), 'Two', false, 'Equals',
+      'Two');
+    explorationEditorMainTab.getResponseEditor('default').setFeedback(
+      forms.toRichText('Try again'));
+
+    explorationEditorMainTab.moveToState('End');
+    explorationEditorMainTab.setInteraction('EndExploration');
+    explorationEditorPage.saveChanges();
+    workflow.publishExploration();
+    general.getExplorationIdFromEditor().then(expId => this.expId = expId);
+
+    adminPage.editConfigProperty(
+      'Exposes the Improvements Tab for creators in the exploration editor.',
+      'Boolean', element => element.setValue(true));
+    adminPage.editConfigProperty(
+      'The set of exploration IDs for recording playthrough issues',
+      'List', element => element.addItem('Unicode').setValue(this.expId));
+    adminPage.editConfigProperty(
+      'The probability of recording playthroughs',
+      'Real', element => element.setValue(1.0));
+  });
+
+  describe('Early Quit playthroughs', function() {
+    describe('resolving the task', function() {
+      it('disappears from the improvements tab', function() {
+        users.login(LEARNER_EMAIL);
+        libraryPage.get();
+        libraryPage.findExploration(EXPLORATION_TITLE);
+        libraryPage.playExploration(EXPLORATION_TITLE);
+
+        explorationPlayerPage.submitAnswer('TextInput', 'One');
+        explorationPlayerPage.clickThroughToNextCard();
+        explorationPlayerPage.expectExplorationToNotBeOver();
+        oppiaLogo.click();
+        general.acceptAlert();
+        users.logout();
+
+        users.login(CREATOR_EMAIL);
+        creatorDashboardPage.get();
+        creatorDashboardPage.navigateToExplorationEditor();
+        explorationEditorPage.navigateToImprovementsTab();
+
+        var task = improvementsTab.getPlaythroughTask(
+          'learners exited the exploration in less than a minute');
+        improvementsTab.clickTaskActionButton(task, 'Mark as Resolved');
+        improvementsTab.confirmAction();
+
+        expect(improvementsTab.getTasks().count()).toEqual(0);
+      });
+    });
+
+    describe('viewing the task', function() {
+      beforeAll(function() {
+        users.login(LEARNER_EMAIL);
+        libraryPage.get();
+        libraryPage.findExploration(EXPLORATION_TITLE);
+        libraryPage.playExploration(EXPLORATION_TITLE);
+
+        explorationPlayerPage.submitAnswer('TextInput', 'One');
+        explorationPlayerPage.clickThroughToNextCard();
+        explorationPlayerPage.expectExplorationToNotBeOver();
+        oppiaLogo.click();
+        general.acceptAlert();
+        users.logout();
+      });
+
+      it('hides the mark as resolve button from guests', function() {
+        users.logout();
+        general.openEditor(this.expId);
+        explorationEditorPage.navigateToImprovementsTab();
+        var task = improvementsTab.getPlaythroughTask(
+          'learners exited the exploration in less than a minute');
+
+        expect(improvementsTab.getTaskActionButtons(task).count()).toEqual(0);
+      });
+    });
+  });
+});
diff --git a/core/tests/protractor_utils/ExplorationEditorImprovementsTab.js b/core/tests/protractor_utils/ExplorationEditorImprovementsTab.js
--- a/core/tests/protractor_utils/ExplorationEditorImprovementsTab.js
+++ b/core/tests/protractor_utils/ExplorationEditorImprovementsTab.js
@@ -30,6 +30,7 @@ var ExplorationEditorImprovementsTab = function() {
     $$('.protractor-test-improvements-thread-message-body');
 
   var onlyOpenInput = $('.protractor-test-improvements-only-open-input');
+  var confirmModalButton = $('.protractor-test-confirm-button');
   var closeModalButton = $('.protractor-test-improvements-close-modal-button');
 
   var answerDetails = $('.protractor-test-improvements-answer-details');
@@ -56,27 +57,35 @@ var ExplorationEditorImprovementsTab = function() {
     by.css('.protractor-test-improvements-action-button');
   var taskBodyLocator = by.css('.protractor-test-improvements-task-body');
   var taskStatusLocator = by.css('.protractor-test-improvements-task-status');
+  var taskTitleLocator = by.css('.protractor-test-improvements-task-title');
   var stateNameLocator =
     by.css('.protractor-test-improvements-task-state-name');
 
-  var _buildTaskStateNameMatcher = function(expectedStateName) {
-    return function(task) {
+  var newTaskStateNameMatcher = (expectedStateName) => {
+    return (task) => {
       return task.element(stateNameLocator).getText()
         .then(stateName => stateName === expectedStateName);
     };
   };
 
-  var _buildTaskTypeMatcher = function(expectedTaskType) {
-    return function(task) {
+  var newTaskTypeMatcher = (expectedTaskType) => {
+    return (task) => {
       return task.getAttribute('class')
         .then(cssClass => cssClass.includes(expectedTaskType));
     };
   };
 
-  var _buildTaskHasContentMatcher = function(expectedContent) {
-    return function(task) {
+  var newTaskContentMatcher = (expectedContent) => {
+    return (task) => {
       return task.element(taskBodyLocator).getText()
-        .then(body => body.includes(expectedContent));
+        .then(taskBody => taskBody.includes(expectedContent));
+    };
+  };
+
+  var newTaskTitleMatcher = (expectedTitle) => {
+    return (task) => {
+      return task.element(taskTitleLocator).getText()
+        .then(taskTitle => taskTitle.includes(expectedTitle));
     };
   };
 
@@ -87,58 +96,80 @@ var ExplorationEditorImprovementsTab = function() {
    * @param {Iterable.<(ElementFinder) => Promise.<boolean>>} matchers
    * @returns {(ElementFinder) => Promise.<boolean>}
    */
-  var _reduceTaskMatchers = function(matchers) {
-    return function(task) {
+  var reduceTaskMatchers = (matchers) => {
+    return (task) => {
       return Promise.all(matchers.map(isMatch => isMatch(task)))
         .then(matchResults => matchResults.every(m => m));
     };
   };
 
-  this.getAnswerDetailsTask = function(stateName) {
-    var answerDetailsTaskMatcher = _reduceTaskMatchers([
-      _buildTaskTypeMatcher('answer-details'),
-      _buildTaskStateNameMatcher(stateName),
+  this.getAnswerDetailsTask = (stateName) => {
+    var answerDetailsTaskMatcher = reduceTaskMatchers([
+      newTaskTypeMatcher('answer-details'),
+      newTaskStateNameMatcher(stateName),
     ]);
     return allTasks.filter(answerDetailsTaskMatcher).first();
   };
 
-  this.getFeedbackTask = function(latestMessage) {
-    var feedbackTaskMatcher = _reduceTaskMatchers([
-      _buildTaskTypeMatcher('feedback'),
-      _buildTaskHasContentMatcher(latestMessage),
+  this.getFeedbackTask = (latestMessage) => {
+    var feedbackTaskMatcher = reduceTaskMatchers([
+      newTaskTypeMatcher('feedback'),
+      newTaskContentMatcher(latestMessage),
     ]);
     return allTasks.filter(feedbackTaskMatcher).first();
   };
 
-  this.getSuggestionTask = function(description) {
-    var suggestionTaskMatcher = _reduceTaskMatchers([
-      _buildTaskTypeMatcher('suggestion'),
-      _buildTaskHasContentMatcher(description),
+  this.getSuggestionTask = (description) => {
+    var suggestionTaskMatcher = reduceTaskMatchers([
+      newTaskTypeMatcher('suggestion'),
+      newTaskContentMatcher(description),
     ]);
     return allTasks.filter(suggestionTaskMatcher).first();
   };
 
-  this.getTaskStatus = function(task) {
+  this.getPlaythroughTask = (taskTitle) => {
+    var playthroughTaskMatcher = reduceTaskMatchers([
+      newTaskTypeMatcher('playthrough'),
+      newTaskTitleMatcher(taskTitle),
+    ]);
+    return allTasks.filter(playthroughTaskMatcher).first();
+  };
+
+  this.getTasks = () => {
+    return allTasks;
+  };
+
+  this.getTaskStatus = (task) => {
     return task.element(taskStatusLocator).getText();
   };
 
-  this.clickTaskActionButton = function(task, buttonText) {
+  this.clickTaskActionButton = (task, buttonText) => {
     var buttonElement = task.element(by.buttonText(buttonText));
     waitFor.elementToBeClickable(
       buttonElement, 'Action button takes too long to become clickable');
     buttonElement.click();
   };
 
-  this.verifyAnswerDetails = function(expectedDetails, expectedInfoCount) {
+  this.getTaskActionButtons = (task) => {
+    return task.all(actionButtonLocator);
+  };
+
+  this.verifyAnswerDetails = (expectedDetails, expectedInfoCount) => {
     expect(answerDetails.getText()).toMatch(expectedDetails);
     expect(answerInfoCount.getText()).toMatch(String(expectedInfoCount));
   };
 
-  this.getThreadMessages = function() {
+  this.verifyReadOnlyAnswerDetails = (expectedDetails, expectedInfoCount) => {
+    this.verifyAnswerDetails(expectedDetails, expectedInfoCount);
+    expect($$('.protractor-test-delete-items').count()).toEqual(0);
+    expect($$('.protractor-test-select-answer-detail').count()).toEqual(0);
+  };
+
+  this.getThreadMessages = () => {
     return allThreadMessages.map(message => message.getText());
   };
 
-  this.sendResponseAndCloseModal = function(feedbackResponse, feedbackStatus) {
+  this.sendResponseAndCloseModal = (feedbackResponse, feedbackStatus) => {
     responseTextarea.sendKeys(feedbackResponse);
     if (feedbackStatus) {
       responseStatusSelect.click();
@@ -147,7 +178,7 @@ var ExplorationEditorImprovementsTab = function() {
     responseSendButton.click();
   };
 
-  this.acceptSuggestion = function() {
+  this.acceptSuggestion = () => {
     waitFor.elementToBeClickable(
       reviewSuggestionButton,
       'View Suggestion button takes too long to become clickable');
@@ -160,7 +191,7 @@ var ExplorationEditorImprovementsTab = function() {
     acceptSuggestionButton.click();
   };
 
-  this.rejectSuggestion = function() {
+  this.rejectSuggestion = () => {
     waitFor.elementToBeClickable(
       reviewSuggestionButton,
       'View Suggestion button takes too long to become clickable');
@@ -173,13 +204,19 @@ var ExplorationEditorImprovementsTab = function() {
     rejectSuggestionButton.click();
   };
 
-  this.setShowOnlyOpenTasks = function(choice = true) {
+  this.setShowOnlyOpenTasks = (choice = true) => {
     if (choice !== onlyOpenInput.isSelected()) {
       onlyOpenInput.click();
     }
   };
 
-  this.closeModal = function() {
+  this.confirmAction = () => {
+    waitFor.elementToBeClickable(
+      confirmModalButton, 'Confirm button takes too long to become clickable');
+    confirmModalButton.click();
+  };
+
+  this.closeModal = () => {
     waitFor.elementToBeClickable(
       closeModalButton, 'Close button takes too long to become clickable');
     closeModalButton.click();