Skip to content

Commit

Permalink
Added story viewer backend handler (oppia#6237)
Browse files Browse the repository at this point in the history
* First

* First

* Second

* Third

* Fourth

* Added new file

* Added test file and changes

* Added test file and running

* Fixed linting issues

* Fixed linting issues

* Fixed linting issues

* running tests success

* merge conflicts removed

* Fixed

* Fixed

* Changes_Done

* Changes_Done
  • Loading branch information
anubhavsinha98 authored and aks681 committed Feb 15, 2019
1 parent 582c910 commit 0f1a9d7
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 0 deletions.
42 changes: 42 additions & 0 deletions core/controllers/acl_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from core.domain import rights_manager
from core.domain import role_services
from core.domain import skill_services
from core.domain import story_services
from core.domain import suggestion_services
from core.domain import topic_services
from core.domain import user_services
Expand Down Expand Up @@ -2352,6 +2353,47 @@ def test_can_access(self, topic_name, **kwargs):
return test_can_access


def can_access_story_viewer_page(handler):
"""Decorator to check whether user can access story viewer page.
Args:
handler: function. The function to be decorated.
Returns:
function. The newly decorated function that now checks
if the user can access the given story viewer page.
"""

def test_can_access(self, story_id, **kwargs):
"""Checks if the user can access story viewer page.
Args:
story_id: str. The unique id of the story.
**kwargs: *. Keyword arguments.
Returns:
*. The return value of the decorated function.
Raises:
PageNotFoundException: The given page cannot be found.
"""
story = story_services.get_story_by_id(story_id, strict=False)

if story is None:
raise self.PageNotFoundException

story_rights = story_services.get_story_rights(
story_id, strict=False)

if story_rights.story_is_published:
return handler(self, story_id, **kwargs)
else:
raise self.PageNotFoundException
test_can_access.__wrapped__ = True

return test_can_access


def get_decorator_for_accepting_suggestion(decorator):
"""Function that takes a decorator as an argument and then applies some
common checks and then checks the permissions specified by the passed in
Expand Down
54 changes: 54 additions & 0 deletions core/controllers/story_viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright 2019 The Oppia Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Controllers for the story viewer page"""

from constants import constants
from core.controllers import acl_decorators
from core.controllers import base
from core.domain import story_services
import feconf


class StoryPageDataHandler(base.BaseHandler):
"""Manages the data that needs to be displayed to a learner on the
story viewer page.
"""
GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON

@acl_decorators.can_access_story_viewer_page
def get(self, story_id):
"""Handles GET requests."""
if not constants.ENABLE_NEW_STRUCTURE_PLAYERS:
raise self.PageNotFoundException

story = story_services.get_story_by_id(story_id)

completed_nodes = [completed_node.to_dict()
for completed_node in
story_services.get_completed_nodes_in_story(
self.user_id, story_id)]

pending_nodes = [pending_node.to_dict()
for pending_node in
story_services.get_pending_nodes_in_story(
self.user_id, story_id)]

self.values.update({
'story_title': story.title,
'story_description': story.description,
'completed_nodes': completed_nodes,
'pending_nodes': pending_nodes
})
self.render_json(self.values)
108 changes: 108 additions & 0 deletions core/controllers/story_viewer_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Copyright 2019 The Oppia Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for the story viewer page"""

from constants import constants
from core.domain import rights_manager
from core.domain import story_domain
from core.domain import story_services
from core.domain import user_services
from core.tests import test_utils
import feconf


class BaseStoryViewerControllerTests(test_utils.GenericTestBase):

def _record_completion(self, user_id, STORY_ID, node_id):
"""Records the completion of a node in the context of a story."""
story_services.record_completed_node_in_story_context(
user_id, STORY_ID, node_id)

def setUp(self):
"""Completes the sign up process for the various users."""
super(BaseStoryViewerControllerTests, self).setUp()
self.VIEWER_EMAIL = '[email protected]'
self.VIEWER_USERNAME = 'viewer'
self.signup(self.ADMIN_EMAIL, self.ADMIN_USERNAME)
self.admin_id = self.get_user_id_from_email(self.ADMIN_EMAIL)
self.set_admins([self.ADMIN_USERNAME])
self.admin = user_services.UserActionsInfo(self.admin_id)
self.login(self.ADMIN_EMAIL)
self.STORY_ID_1 = 'story_id_1'
self.NODE_ID_1 = 'node_1'
self.NODE_ID_2 = 'node_2'
self.EXP_ID = 'exp_id'

self.save_new_valid_exploration(
self.EXP_ID, self.admin_id, title='Bridges in England',
category='Architecture', language_code='en')
rights_manager.publish_exploration(self.admin, self.EXP_ID)
story = story_domain.Story.create_default_story(
self.STORY_ID_1, 'Title')
story.description = ('Description')
self.node_1 = {
'id': self.NODE_ID_1,
'title': 'Title 1',
'destination_node_ids': ['node_2'],
'acquired_skill_ids': [],
'prerequisite_skill_ids': [],
'outline': '',
'outline_is_finalized': False,
'exploration_id': self.EXP_ID
}
self.node_2 = {
'id': self.NODE_ID_2,
'title': 'Title 2',
'destination_node_ids': [],
'acquired_skill_ids': [],
'prerequisite_skill_ids': [],
'outline': '',
'outline_is_finalized': False,
'exploration_id': self.EXP_ID
}
story.story_contents.nodes = [
story_domain.StoryNode.from_dict(self.node_1),
story_domain.StoryNode.from_dict(self.node_2)
]
self.nodes = story.story_contents.nodes
story.story_contents.initial_node_id = 'node_1'
story.story_contents.next_node_id = 'node_3'
story_services.save_new_story(self.admin_id, story)
story_services.publish_story(self.STORY_ID_1, self.admin_id)
self.logout()
self.signup(self.VIEWER_EMAIL, self.VIEWER_USERNAME)
self.viewer_id = self.get_user_id_from_email(self.VIEWER_EMAIL)
self.login(self.VIEWER_EMAIL)
self._record_completion(self.viewer_id, self.STORY_ID_1, self.NODE_ID_1)


class StoryPageDataHandlerTests(BaseStoryViewerControllerTests):
def test_get(self):
with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', True):
json_response = self.get_json(
'%s/%s' % (feconf.STORY_DATA_HANDLER, 'story_id_1'))
expected_dict = {
'story_title': 'Title',
'story_description': 'Description',
'completed_nodes': [self.node_1],
'pending_nodes': [self.node_2]
}
self.assertDictContainsSubset(expected_dict, json_response)

def test_get_fails_when_new_structures_not_enabled(self):
with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', False):
self.get_json(
'%s/%s' % (feconf.STORY_DATA_HANDLER, 'story_id_1'),
expected_status_int=404)
22 changes: 22 additions & 0 deletions core/domain/story_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,28 @@ def get_completed_nodes_in_story(user_id, story_id):
return completed_nodes


def get_pending_nodes_in_story(user_id, story_id):
"""Returns the nodes that are pending in a story
Args:
user_id: str. The user id of the user.
story_id: str. The id of the story.
Returns:
list(StoryNode): The list of story nodes, pending
for the user.
"""
story = get_story_by_id(story_id)
pending_nodes = []

completed_node_ids = get_completed_node_ids(user_id, story_id)
for node in story.story_contents.nodes:
if node.id not in completed_node_ids:
pending_nodes.append(node)

return pending_nodes


def record_completed_node_in_story_context(user_id, story_id, node_id):
"""Records a node by a given user in a given story
context as having been played.
Expand Down
9 changes: 9 additions & 0 deletions core/domain/story_services_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,15 @@ def test_get_completed_nodes_in_story(self):
self.assertEqual(
completed_node.to_dict(), self.nodes[ind].to_dict())

def test_get_pending_nodes_in_story(self):
self._record_completion(self.owner_id, self.STORY_1_ID, self.NODE_ID_1)

for _, pending_node in enumerate(
story_services.get_pending_nodes_in_story(
self.owner_id, self.STORY_1_ID)):
self.assertEqual(
pending_node.to_dict(), self.nodes[1].to_dict())

def test_record_completed_node_in_story_context(self):
# Ensure that node completed within the context of a story are
# recorded correctly. This test actually validates both
Expand Down
1 change: 1 addition & 0 deletions feconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,7 @@ def get_empty_ratings():
SKILL_RIGHTS_URL_PREFIX = '/skill_editor_handler/rights'
SKILL_PUBLISH_URL_PREFIX = '/skill_editor_handler/publish_skill'
SPLASH_URL = '/splash'
STORY_DATA_HANDLER = '/story_data_handler'
STORY_EDITOR_URL_PREFIX = '/story_editor'
STORY_EDITOR_DATA_URL_PREFIX = '/story_editor_handler/data'
SUGGESTION_ACTION_URL_PREFIX = '/suggestionactionhandler'
Expand Down
4 changes: 4 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from core.controllers import resources
from core.controllers import skill_editor
from core.controllers import story_editor
from core.controllers import story_viewer
from core.controllers import subscriptions
from core.controllers import suggestion
from core.controllers import topic_editor
Expand Down Expand Up @@ -225,6 +226,9 @@ def ui_access_wrapper(self, *args, **kwargs):
get_redirect_route(
r'%s/<skill_id>' % feconf.NEW_QUESTION_URL,
question_editor.QuestionCreationHandler),
get_redirect_route(
r'%s/<story_id>' % feconf.STORY_DATA_HANDLER,
story_viewer.StoryPageDataHandler),
get_redirect_route(
r'%s/<topic_id>' % feconf.TOPIC_EDITOR_STORY_URL,
topic_editor.TopicEditorStoryHandler),
Expand Down

0 comments on commit 0f1a9d7

Please sign in to comment.