Repository: oppia/oppia

Issue:
`StorySummaryModelAuditOneOffJob` is producing error
When the `StorySummaryModelAuditOneOffJob` is run error shows up in the console

```
  File "/base/data/home/apps/s~oppiaserver-backup-migration/test-david-dec21.431809448395909174/core/domain/base_model_validators.py", line 507, in validate
    super(BaseSummaryModelValidator, cls).validate(item)
  File "/base/data/home/apps/s~oppiaserver-backup-migration/test-david-dec21.431809448395909174/core/domain/base_model_validators.py", line 382, in validate
    func(item, cls.field_name_to_external_model_references)
  File "/base/data/home/apps/s~oppiaserver-backup-migration/test-david-dec21.431809448395909174/core/domain/story_validators.py", line 193, in _validate_node_titles
    node_titles = [node.title for node in nodes]
AttributeError: 'dict' object has no attribute 'title'
```
`TopicSimilaritiesModelAuditOneOffJob` failing to run on the test server
The `TopicSimilaritiesModelAuditOneOffJob` is failing on the test server with this error:
```
Traceback (most recent call last): (/base/data/home/apps/s~oppiatestserver/3-0-6.432116405664477709/third_party/python_libs/mapreduce/handlers.py:517)
  File "/base/data/home/apps/s~oppiatestserver/3-0-6.432116405664477709/third_party/python_libs/mapreduce/handlers.py", line 503, in handle
    tstate.input_reader, shard_state, tstate, ctx)
  File "/base/data/home/apps/s~oppiatestserver/3-0-6.432116405664477709/third_party/python_libs/mapreduce/handlers.py", line 593, in _process_inputs
    entity, input_reader, ctx, tstate):
  File "/base/data/home/apps/s~oppiatestserver/3-0-6.432116405664477709/third_party/python_libs/mapreduce/handlers.py", line 635, in _process_datum
    for output in result:
  File "/base/data/home/apps/s~oppiatestserver/3-0-6.432116405664477709/core/domain/prod_validation_jobs_one_off.py", line 118, in map
    validator.validate(model_instance)
  File "/base/data/home/apps/s~oppiatestserver/3-0-6.432116405664477709/core/domain/base_model_validators.py", line 379, in validate
    func(item)
  File "/base/data/home/apps/s~oppiatestserver/3-0-6.432116405664477709/core/domain/recommendations_validators.py", line 90, in _validate_topic_similarities
    all_topics = list(item.content.keys())
AttributeError: 'unicode' object has no attribute 'keys'
```


Discussion:

Resolving patch:
diff --git a/core/domain/base_model_validators.py b/core/domain/base_model_validators.py
--- a/core/domain/base_model_validators.py
+++ b/core/domain/base_model_validators.py
@@ -62,7 +62,7 @@
 ERROR_CATEGORY_TYPE_CHECK = 'type check'
 ERROR_CATEGORY_VERSION_CHECK = 'version check'
 ERROR_CATEGORY_STALE_CHECK = 'stale check'
-ERROR_CATEGORY_INVALID_USER_SETTING_IDS = 'invalid user setting ids'
+ERROR_CATEGORY_INVALID_IDS_IN_FIELD = 'invalid ids in field'
 
 VALIDATION_MODE_NEUTRAL = 'neutral'
 VALIDATION_MODE_STRICT = 'strict'
@@ -97,9 +97,19 @@ def __init__(self, field_name, model_class, model_ids):
                 'When fetching instances of UserSettingsModel, please use ' +
                 'UserSettingsModelFetcherDetails instead of ' +
                 'ExternalModelFetcherDetails')
+        validated_model_ids = []
+        model_id_errors = []
+        for model_id in model_ids:
+            if not model_id:
+                model_id_errors.append(
+                    'A model id in the field \'%s\' '
+                    'is empty' % field_name)
+            else:
+                validated_model_ids.append(model_id)
         self.field_name = field_name
         self.model_class = model_class
-        self.model_ids = model_ids
+        self.model_ids = validated_model_ids
+        self.model_id_errors = model_id_errors
 
 
 class UserSettingsModelFetcherDetails(python_utils.OBJECT):
@@ -130,30 +140,29 @@ def __init__(
                 fields can only be in models that have LOCALLY_PSEUDONYMIZE as
                 their DELETION_POLICY.
         """
-        filtered_model_ids = model_ids
-        if may_contain_system_ids:
-            filtered_model_ids = list(
-                set(filtered_model_ids) - set(feconf.SYSTEM_USERS.values()))
-        else:
-            if set(filtered_model_ids) & set(feconf.SYSTEM_USERS.values()):
-                raise utils.ValidationError(
-                    'The field \'%s\' should not contain '
-                    'system IDs' % field_name)
-        if may_contain_pseudonymous_ids:
-            filtered_model_ids = [
-                model_id for model_id in filtered_model_ids
-                if not utils.is_pseudonymous_id(model_id)
-            ]
-        else:
-            if any(
-                    utils.is_pseudonymous_id(model_id)
-                    for model_id in filtered_model_ids):
-                raise utils.ValidationError(
-                    'The field \'%s\' should not contain '
-                    'pseudonymous IDs' % field_name)
+        model_id_errors = []
+        validated_model_ids = []
+        for model_id in model_ids:
+            if model_id in feconf.SYSTEM_USERS.values():
+                if not may_contain_system_ids:
+                    model_id_errors.append(
+                        'The field \'%s\' should not contain '
+                        'system IDs' % field_name)
+            elif utils.is_pseudonymous_id(model_id):
+                if not may_contain_pseudonymous_ids:
+                    model_id_errors.append(
+                        'The field \'%s\' should not contain '
+                        'pseudonymous IDs' % field_name)
+            elif not utils.is_user_id_valid(model_id):
+                model_id_errors.append(
+                    'The user id %s in the field \'%s\' is '
+                    'invalid' % (model_id, field_name))
+            else:
+                validated_model_ids.append(model_id)
         self.field_name = field_name
         self.model_class = user_models.UserSettingsModel
-        self.model_ids = filtered_model_ids
+        self.model_ids = validated_model_ids
+        self.model_id_errors = model_id_errors
 
 
 class ExternalModelReference(python_utils.OBJECT):
@@ -347,34 +356,33 @@ def _fetch_field_name_to_external_model_references(cls, item):
         """
         multiple_models_ids_to_fetch = {}
 
-        try:
-            for external_model_fetcher_details in (
-                    cls._get_external_id_relationships(item)):
-                multiple_models_ids_to_fetch[
-                    external_model_fetcher_details.field_name] = (
-                        external_model_fetcher_details.model_class,
-                        external_model_fetcher_details.model_ids)
-        except utils.ValidationError as err:
-            cls._add_error(
-                ERROR_CATEGORY_INVALID_USER_SETTING_IDS,
-                'Entity id %s: %s' % (item.id, python_utils.UNICODE(err)))
-        else:
-            fetched_model_instances_for_all_ids = (
-                datastore_services.fetch_multiple_entities_by_ids_and_models(
-                    list(multiple_models_ids_to_fetch.values())))
-
-            for index, field_name in enumerate(multiple_models_ids_to_fetch):
-                (model_class, model_ids) = (
-                    multiple_models_ids_to_fetch[field_name])
-                fetched_model_instances = fetched_model_instances_for_all_ids[
-                    index]
-
-                for (model_id, model_instance) in python_utils.ZIP(
-                        model_ids, fetched_model_instances):
-                    cls.field_name_to_external_model_references[
-                        field_name].append(
-                            ExternalModelReference(
-                                model_class, model_id, model_instance))
+        for external_model_fetcher_details in (
+                cls._get_external_id_relationships(item)):
+            for error in external_model_fetcher_details.model_id_errors:
+                cls._add_error(
+                    ERROR_CATEGORY_INVALID_IDS_IN_FIELD,
+                    'Entity id %s: %s' % (item.id, error))
+            multiple_models_ids_to_fetch[
+                external_model_fetcher_details.field_name] = (
+                    external_model_fetcher_details.model_class,
+                    external_model_fetcher_details.model_ids)
+
+        fetched_model_instances_for_all_ids = (
+            datastore_services.fetch_multiple_entities_by_ids_and_models(
+                list(multiple_models_ids_to_fetch.values())))
+
+        for index, field_name in enumerate(multiple_models_ids_to_fetch):
+            (model_class, model_ids) = (
+                multiple_models_ids_to_fetch[field_name])
+            fetched_model_instances = (
+                fetched_model_instances_for_all_ids[index])
+
+            for (model_id, model_instance) in python_utils.ZIP(
+                    model_ids, fetched_model_instances):
+                cls.field_name_to_external_model_references[
+                    field_name].append(
+                        ExternalModelReference(
+                            model_class, model_id, model_instance))
 
     @classmethod
     def _validate_model_time_fields(cls, item):
diff --git a/core/domain/recommendations_validators.py b/core/domain/recommendations_validators.py
--- a/core/domain/recommendations_validators.py
+++ b/core/domain/recommendations_validators.py
@@ -19,6 +19,8 @@
 from __future__ import absolute_import  # pylint: disable=import-only-modules
 from __future__ import unicode_literals  # pylint: disable=import-only-modules
 
+import json
+
 from core.domain import base_model_validators
 from core.domain import recommendations_services
 from core.platform import models
@@ -87,15 +89,16 @@ def _validate_topic_similarities(cls, item):
                 to validate.
         """
 
-        all_topics = list(item.content.keys())
+        content = json.loads(item.content)
+        all_topics = list(content.keys())
         data = '%s\n' % ','.join(all_topics)
 
         for topics_to_compare in all_topics:
             similarity_list = []
-            for topic in item.content[topics_to_compare]:
+            for topic in content[topics_to_compare]:
                 similarity_list.append(
                     python_utils.UNICODE(
-                        item.content[topics_to_compare][topic]))
+                        content[topics_to_compare][topic]))
             if len(similarity_list):
                 data = data + '%s\n' % ','.join(similarity_list)
 
@@ -105,7 +108,7 @@ def _validate_topic_similarities(cls, item):
             cls._add_error(
                 'topic similarity check',
                 'Entity id %s: Topic similarity validation for content: %s '
-                'fails with error: %s' % (item.id, item.content, e))
+                'fails with error: %s' % (item.id, content, e))
 
     @classmethod
     def _get_custom_validation_functions(cls):
diff --git a/core/domain/story_validators.py b/core/domain/story_validators.py
--- a/core/domain/story_validators.py
+++ b/core/domain/story_validators.py
@@ -196,7 +196,7 @@ def _validate_node_titles(
                         item.id, model_id, model_class.__name__, model_id))
                 continue
             nodes = story_model.story_contents['nodes']
-            node_titles = [node.title for node in nodes]
+            node_titles = [node['title'] for node in nodes]
             if item.node_titles != node_titles:
                 cls._add_error(
                     'node titles check',


Test patch:
diff --git a/core/domain/base_model_validators_test.py b/core/domain/base_model_validators_test.py
--- a/core/domain/base_model_validators_test.py
+++ b/core/domain/base_model_validators_test.py
@@ -27,7 +27,6 @@
 from core.platform import models
 from core.tests import test_utils
 import feconf
-import utils
 
 (base_models, user_models) = models.Registry.import_models(
     [models.NAMES.base_model, models.NAMES.user])
@@ -141,6 +140,8 @@ def _get_external_id_relationships(cls, item):
 
 class BaseValidatorTests(test_utils.AuditJobsTestBase):
 
+    USER_ID = 'uid_%s' % ('a' * 32)
+
     def setUp(self):
         super(BaseValidatorTests, self).setUp()
         self.invalid_model = MockModel(id='mockmodel')
@@ -233,53 +234,79 @@ def test_external_model_fetcher_with_user_settings_raise_error(self):
             base_model_validators.ExternalModelFetcherDetails(
                 'committer_ids', user_models.UserSettingsModel,
                 [
-                    feconf.MIGRATION_BOT_USER_ID, 'User-1',
+                    feconf.MIGRATION_BOT_USER_ID, self.USER_ID,
                     self.PSEUDONYMOUS_ID
                 ]
             )
 
+    def test_external_model_fetcher_with_invalid_id(self):
+        external_model = base_model_validators.ExternalModelFetcherDetails(
+            'mock_field', MockModel, ['', 'user-1']
+        )
+        self.assertItemsEqual(external_model.model_ids, ['user-1'])
+        self.assertItemsEqual(
+            external_model.model_id_errors,
+            ['A model id in the field \'mock_field\' is empty'])
+
+    def test_user_setting_model_fetcher_with_invalid_id(self):
+        user_settings_model = (
+            base_model_validators.UserSettingsModelFetcherDetails(
+                'mock_field', ['User-1', self.USER_ID],
+                may_contain_system_ids=False,
+                may_contain_pseudonymous_ids=False
+            ))
+        self.assertItemsEqual(user_settings_model.model_ids, [self.USER_ID])
+        self.assertItemsEqual(
+            user_settings_model.model_id_errors,
+            ['The user id User-1 in the field \'mock_field\' is invalid'])
+
     def test_may_contain_system_users_filters_system_ids(self):
         user_settings_model = (
             base_model_validators.UserSettingsModelFetcherDetails(
                 'committer_ids',
-                [feconf.MIGRATION_BOT_USER_ID, 'User-1'],
+                [feconf.MIGRATION_BOT_USER_ID, self.USER_ID],
                 may_contain_system_ids=True,
                 may_contain_pseudonymous_ids=False
             ))
 
         self.assertItemsEqual(
-            user_settings_model.model_ids, ['User-1'])
+            user_settings_model.model_ids, [self.USER_ID])
 
-    def test_error_raised_if_model_ids_contain_system_ids(self):
-        with self.assertRaisesRegexp(
-            utils.ValidationError,
-            'The field \'committer_ids\' should not contain system IDs'):
+    def test_user_setting_model_fetcher_with_system_id(self):
+        user_settings_model = (
             base_model_validators.UserSettingsModelFetcherDetails(
-                'committer_ids', [feconf.MIGRATION_BOT_USER_ID, 'User-1'],
+                'committer_ids', [
+                    feconf.MIGRATION_BOT_USER_ID, self.USER_ID],
                 may_contain_system_ids=False,
                 may_contain_pseudonymous_ids=False
-            )
+            ))
+        self.assertItemsEqual(user_settings_model.model_ids, [self.USER_ID])
+        self.assertItemsEqual(
+            user_settings_model.model_id_errors,
+            ['The field \'committer_ids\' should not contain system IDs'])
 
     def test_may_contain_pseudonymous_users_filters_pseudonymous_users(self):
         user_settings_model = (
             base_model_validators.UserSettingsModelFetcherDetails(
-                'committer_ids', ['User-1', self.PSEUDONYMOUS_ID],
+                'committer_ids', [self.USER_ID, self.PSEUDONYMOUS_ID],
                 may_contain_system_ids=False,
                 may_contain_pseudonymous_ids=True
             ))
 
         self.assertItemsEqual(
-            user_settings_model.model_ids, ['User-1'])
+            user_settings_model.model_ids, [self.USER_ID])
 
     def test_error_raised_if_model_ids_contain_pseudonymous_ids(self):
-        with self.assertRaisesRegexp(
-            utils.ValidationError,
-            'The field \'committer_ids\' should not contain pseudonymous IDs'):
+        user_settings_model = (
             base_model_validators.UserSettingsModelFetcherDetails(
-                'committer_ids', [self.PSEUDONYMOUS_ID, 'User-1'],
+                'committer_ids', [self.PSEUDONYMOUS_ID, self.USER_ID],
                 may_contain_system_ids=False,
                 may_contain_pseudonymous_ids=False
-            )
+            ))
+        self.assertItemsEqual(user_settings_model.model_ids, [self.USER_ID])
+        self.assertItemsEqual(
+            user_settings_model.model_id_errors,
+            ['The field \'committer_ids\' should not contain pseudonymous IDs'])
 
     def test_error_raised_when_fetching_external_model_with_system_ids(self):
         model = MockCommitLogEntryModel(
@@ -292,7 +319,7 @@ def test_error_raised_when_fetching_external_model_with_system_ids(self):
         mock_validator.validate(model)
         self.assertDictContainsSubset(
             {
-                'invalid user setting ids': [
+                'invalid ids in field': [
                     'Entity id mock-12345: '
                     'The field \'user_id\' should not contain system IDs'
                 ]
@@ -311,10 +338,30 @@ def test_error_raised_when_fetching_external_model_with_pseudo_ids(self):
         mock_validator.validate(model)
         self.assertDictContainsSubset(
             {
-                'invalid user setting ids': [
+                'invalid ids in field': [
                     'Entity id mock-12345: '
                     'The field \'user_id\' should not contain pseudonymous IDs'
                 ]
             },
             mock_validator.errors
         )
+
+    def test_error_raised_when_user_id_is_invalid(self):
+        model = MockCommitLogEntryModel(
+            id='mock-12345',
+            user_id='invalid_user_id',
+            commit_cmds=[])
+        model.update_timestamps()
+        mock_validator = MockCommitLogEntryModelValidator()
+        mock_validator.errors.clear()
+        mock_validator.validate(model)
+        self.assertDictContainsSubset(
+            {
+                'invalid ids in field': [
+                    'Entity id mock-12345: '
+                    'The user id invalid_user_id in the field \'user_id\' is '
+                    'invalid'
+                ]
+            },
+            mock_validator.errors
+        )
diff --git a/core/domain/email_validators_test.py b/core/domain/email_validators_test.py
--- a/core/domain/email_validators_test.py
+++ b/core/domain/email_validators_test.py
@@ -48,7 +48,7 @@ def mock_generate_hash(
             return 'Email Hash'
 
         self.sender_email = 'noreply@oppia.org'
-        self.sender_id = 'sender'
+        self.sender_id = 'uid_sender%s' % ('a' * 26)
         self.sender_model = user_models.UserSettingsModel(
             id=self.sender_id,
             email=self.sender_email)
@@ -56,7 +56,7 @@ def mock_generate_hash(
         self.sender_model.put()
 
         self.recipient_email = 'recipient@email.com'
-        self.recipient_id = 'recipient'
+        self.recipient_id = 'uid_recipient%s' % ('a' * 23)
         self.recipient_model = user_models.UserSettingsModel(
             id=self.recipient_id,
             email=self.recipient_email)
@@ -207,14 +207,15 @@ def setUp(self):
         super(BulkEmailModelValidatorTests, self).setUp()
 
         self.sender_email = 'sender@email.com'
-        self.sender_id = 'sender'
+        self.sender_id = 'uid_sender%s' % ('a' * 26)
         self.sender_model = user_models.UserSettingsModel(
             id=self.sender_id,
             email=self.sender_email)
         self.sender_model.update_timestamps()
         self.sender_model.put()
 
-        self.recipient_ids = ['recipient1', 'recipient2']
+        self.recipient_ids = [
+            'uid_recipient%s' % ('a' * 23), 'uid_recipient%s' % ('b' * 23)]
         self.recipient_model_1 = user_models.UserSettingsModel(
             id=self.recipient_ids[0],
             email='recipient1@email.com')
diff --git a/core/domain/improvements_validators_test.py b/core/domain/improvements_validators_test.py
--- a/core/domain/improvements_validators_test.py
+++ b/core/domain/improvements_validators_test.py
@@ -40,6 +40,8 @@
 
 class TaskEntryModelValidatorTests(test_utils.AuditJobsTestBase):
 
+    USER_ID = 'uid_%s' % ('a' * 32)
+
     def setUp(self):
         super(TaskEntryModelValidatorTests, self).setUp()
         self.signup(USER_EMAIL, USER_NAME)
@@ -131,14 +133,15 @@ def test_invalid_resolver_ids(self):
             feconf.DEFAULT_INIT_STATE_NAME,
             issue_description='issue description',
             status=improvements_models.TASK_STATUS_RESOLVED,
-            resolver_id='invalid_user_id',
+            resolver_id=self.USER_ID,
             resolved_on=CURRENT_DATETIME)
         self.run_job_and_check_output(
             ['failed validation check for resolver_ids field check of '
              'TaskEntryModel',
              ['Entity id %s: based on field resolver_ids having value '
-              'invalid_user_id, expected model UserSettingsModel with id '
-              'invalid_user_id but it doesn\'t exist' % (task_id,)]])
+              '%s, expected model UserSettingsModel with id '
+              '%s but it doesn\'t exist' % (
+                  task_id, self.USER_ID, self.USER_ID)]])
 
     def test_invalid_id(self):
         improvements_models.TaskEntryModel(
diff --git a/core/domain/recommendations_validators_test.py b/core/domain/recommendations_validators_test.py
--- a/core/domain/recommendations_validators_test.py
+++ b/core/domain/recommendations_validators_test.py
@@ -20,6 +20,7 @@
 from __future__ import unicode_literals  # pylint: disable=import-only-modules
 
 import datetime
+import json
 
 from core.domain import exp_domain
 from core.domain import exp_services
@@ -152,11 +153,12 @@ def setUp(self):
         self.model_instance = recommendations_models.TopicSimilaritiesModel(
             id=recommendations_models.TOPIC_SIMILARITIES_ID)
 
-        self.content = {
+        content_dict = {
             'Art': {'Art': '1.0', 'Biology': '0.8', 'Chemistry': '0.1'},
             'Biology': {'Art': '0.8', 'Biology': '1.0', 'Chemistry': '0.5'},
             'Chemistry': {'Art': '0.1', 'Biology': '0.5', 'Chemistry': '1.0'},
         }
+        self.content = json.dumps(content_dict)
 
         self.model_instance.content = self.content
         self.model_instance.update_timestamps()
@@ -221,11 +223,11 @@ def test_model_with_invalid_id(self):
             expected_output, sort=True, literal_eval=False)
 
     def test_model_with_invalid_topic_similarities_columns(self):
-        content = {
+        content_dict = {
             'Art': {'Art': '1.0', 'Biology': '0.5'},
             'Biology': {}
         }
-        self.model_instance.content = content
+        self.model_instance.content = json.dumps(content_dict)
         self.model_instance.update_timestamps()
         self.model_instance.put()
 
@@ -242,11 +244,11 @@ def test_model_with_invalid_topic_similarities_columns(self):
             expected_output, sort=True, literal_eval=False)
 
     def test_model_with_invalid_topic(self):
-        content = {
+        content_dict = {
             'Art': {'Art': '1.0', 'invalid': '0.5'},
             'invalid': {'Art': '0.5', 'invalid': '1.0'}
         }
-        self.model_instance.content = content
+        self.model_instance.content = json.dumps(content_dict)
         self.model_instance.update_timestamps()
         self.model_instance.put()
 
@@ -262,10 +264,10 @@ def test_model_with_invalid_topic(self):
             expected_output, sort=False, literal_eval=False)
 
     def test_model_with_invalid_topic_similarities_rows(self):
-        content = {
+        content_dict = {
             'Art': {'Art': '1.0', 'Biology': '0.5'}
         }
-        self.model_instance.content = content
+        self.model_instance.content = json.dumps(content_dict)
         self.model_instance.update_timestamps()
         self.model_instance.put()
 
@@ -281,11 +283,11 @@ def test_model_with_invalid_topic_similarities_rows(self):
             expected_output, sort=False, literal_eval=False)
 
     def test_model_with_invalid_similarity_type(self):
-        content = {
+        content_dict = {
             'Art': {'Art': 'one', 'Biology': 0.5},
             'Biology': {'Art': 0.5, 'Biology': 1.0}
         }
-        self.model_instance.content = content
+        self.model_instance.content = json.dumps(content_dict)
         self.model_instance.update_timestamps()
         self.model_instance.put()
 
@@ -302,11 +304,11 @@ def test_model_with_invalid_similarity_type(self):
             expected_output, sort=True, literal_eval=False)
 
     def test_model_with_invalid_similarity_value(self):
-        content = {
+        content_dict = {
             'Art': {'Art': 10.0, 'Biology': 0.5},
             'Biology': {'Art': 0.5, 'Biology': 1.0}
         }
-        self.model_instance.content = content
+        self.model_instance.content = json.dumps(content_dict)
         self.model_instance.update_timestamps()
         self.model_instance.put()
 
@@ -323,11 +325,11 @@ def test_model_with_invalid_similarity_value(self):
             expected_output, sort=False, literal_eval=False)
 
     def test_model_with_assymetric_content(self):
-        content = {
+        content_dict = {
             'Art': {'Art': 1.0, 'Biology': 0.5},
             'Biology': {'Art': 0.6, 'Biology': 1.0}
         }
-        self.model_instance.content = content
+        self.model_instance.content = json.dumps(content_dict)
         self.model_instance.update_timestamps()
         self.model_instance.put()
 
diff --git a/core/domain/story_validators_test.py b/core/domain/story_validators_test.py
--- a/core/domain/story_validators_test.py
+++ b/core/domain/story_validators_test.py
@@ -909,6 +909,18 @@ def test_standard_operation(self):
         self.run_job_and_check_output(
             expected_output, sort=False, literal_eval=False)
 
+    def test_node_titles_in_story_models(self):
+        story_id = story_services.get_new_story_id()
+        story = story_domain.Story.create_default_story(
+            story_id, 'Title', 'Description', 'topic_id', 'url')
+        story.add_node('node_1', 'Node title')
+        story_services.save_new_story(self.owner_id, story)
+        story_model = story_models.StoryModel.get_by_id(story_id)
+        nodes = story_model.story_contents['nodes']
+        with self.assertRaisesRegexp(
+            Exception, '\'dict\' object has no attribute \'title\''):
+            _ = [node.title for node in nodes]
+
     def test_model_with_created_on_greater_than_last_updated(self):
         self.model_instance_0.created_on = (
             self.model_instance_0.last_updated + datetime.timedelta(days=1))