# coding: utf-8
#
# Copyright 2014 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, softwar
# 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.

"""Rules for CodeEvaluation objects."""

from extensions.rules import base


class CodeEquals(base.CodeEvaluationRule):
    description = 'has code equal to {{x|CodeString}}'


class CodeContains(base.CodeEvaluationRule):
    description = 'has code that contains {{x|CodeString}}'


class CodeDoesNotContain(base.CodeEvaluationRule):
    description = 'has code that does not contain {{x|CodeString}}'


class OutputEquals(base.CodeEvaluationRule):
    description = 'has output equal to {{x|CodeString}}'


class ResultsInError(base.CodeEvaluationRule):
    description = 'results in an error when run'


class ErrorContains(base.CodeEvaluationRule):
    description = (
        'has error message that contains {{x|UnicodeString}}')

extensions/rules/code_evaluation.py
# coding: utf-8
#
# Copyright 2014 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, softwar
# 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.

"""Rules for CodeEvaluation objects."""

from extensions.rules import base


class CodeEquals(base.CodeEvaluationRule):
    description = 'has code equal to {{x|CodeString}}'


class CodeContains(base.CodeEvaluationRule):
    description = 'has code that contains {{x|CodeString}}'


class CodeDoesNotContain(base.CodeEvaluationRule):
    description = 'has code that does not contain {{x|CodeString}}'

class OutputContains(base.CodeEvaluationRule):
    description = 'has output that contains {{x|CodeString}}'

class OutputEquals(base.CodeEvaluationRule):
    description = 'has output equal to {{x|CodeString}}'


class ResultsInError(base.CodeEvaluationRule):
    description = 'results in an error when run'


class ErrorContains(base.CodeEvaluationRule):
    description = (
        'has error message that contains {{x|UnicodeString}}')

extensions/rules/code_evaluation.py
diff --git a/core/domain/rule_domain_test.py b/core/domain/rule_domain_test.py
index 5afcf9a036f7d..ced4e9429bd7f 100644
--- a/core/domain/rule_domain_test.py
+++ b/core/domain/rule_domain_test.py
@@ -26,7 +26,7 @@
 import feconf
 
 
-EXPECTED_TOTAL_NUMBER_OF_RULES = 47
+EXPECTED_TOTAL_NUMBER_OF_RULES = 48
 
 
 class FakeRule(rule_domain.Rule):
diff --git a/extensions/interactions/CodeRepl/CodeRepl.js b/extensions/interactions/CodeRepl/CodeRepl.js
index fc5b8420d9bd6..d88f035b07ecf 100644
--- a/extensions/interactions/CodeRepl/CodeRepl.js
+++ b/extensions/interactions/CodeRepl/CodeRepl.js
@@ -268,6 +268,11 @@ oppia.factory('codeReplRulesService', [
         codeNormalizationService.getNormalizedCode(inputs.x);
       return normalizedCode.indexOf(normalizedSnippet) == -1;
     },
+    OutputContains: function(answer, inputs) {
+      var normalizedOutput = $filter('normalizeWhitespace')(answer.output);
+      var normalizedSnippet = $filter('normalizeWhitespace')(inputs.x);
+      return normalizedOutput.indexOf(normalizedSnippet) != -1;
+    },
     OutputEquals: function(answer, inputs) {
       var normalizedOutput = $filter('normalizeWhitespace')(answer.output);
       var normalizedExpectedOutput =
diff --git a/extensions/interactions/CodeRepl/CodeReplRulesServiceSpec.js b/extensions/interactions/CodeRepl/CodeReplRulesServiceSpec.js
index c0c81a08d1323..082955c3b25c7 100644
--- a/extensions/interactions/CodeRepl/CodeReplRulesServiceSpec.js
+++ b/extensions/interactions/CodeRepl/CodeReplRulesServiceSpec.js
@@ -148,6 +148,65 @@ describe('Code REPL rules service', function() {
     });
   });
 
+  describe('\'output contains\' rule', function() {
+    var RULE_INPUT = {
+      x: '1'
+    };
+
+    var RULE_INPUT_1 = {
+      x: 'a b c'
+    };
+
+    var RULE_INPUT_2 = {
+      x: 'a\nb\nc'
+    };
+
+    it('should check if output contains some content', function() {
+      expect(crrs.OutputContains({
+        output: '1 2 3 4'
+      }, RULE_INPUT)).toBe(true);
+      expect(crrs.OutputContains({
+        output: '\n1\n2\n3\n4\n'
+      }, RULE_INPUT)).toBe(true);
+      expect(crrs.OutputContains({
+        output: ''
+      }, RULE_INPUT)).toBe(false);
+      expect(crrs.OutputContains({
+        output: 'bad output'
+      }, RULE_INPUT)).toBe(false);
+      expect(crrs.OutputContains({
+        output: 'a b c d e'
+      }, RULE_INPUT_1)).toBe(true);
+      expect(crrs.OutputContains({
+        output: 'a\nb\nc\nd\n'
+      }, RULE_INPUT_1)).toBe(false);
+      expect(crrs.OutputContains({
+        output: 'ab\nc\n'
+      }, RULE_INPUT_1)).toBe(false);
+      expect(crrs.OutputContains({
+        output: ''
+      }, RULE_INPUT_1)).toBe(false);
+      expect(crrs.OutputContains({
+        output: 'bad output'
+      }, RULE_INPUT_1)).toBe(false);
+      expect(crrs.OutputContains({
+        output: 'a\nb\nc\nd\ne'
+      }, RULE_INPUT_2)).toBe(true);
+      expect(crrs.OutputContains({
+        output: '\nabc\ndef\nfgh\n'
+      }, RULE_INPUT_2)).toBe(false);
+      expect(crrs.OutputContains({
+        output: 'a b c'
+      }, RULE_INPUT_2)).toBe(false);
+      expect(crrs.OutputContains({
+        output: ''
+      }, RULE_INPUT_2)).toBe(false);
+      expect(crrs.OutputContains({
+        output: 'bad output'
+      }, RULE_INPUT_2)).toBe(false);
+    });
+  });
+
   describe('\'output equals\' rule', function() {
     var RULE_INPUT = {
       x: '1'
diff --git a/extensions/rules/code_evaluation.py b/extensions/rules/code_evaluation.py
index 829a9a97c2a6e..0055a7c02526a 100644
--- a/extensions/rules/code_evaluation.py
+++ b/extensions/rules/code_evaluation.py
@@ -30,6 +30,8 @@ class CodeContains(base.CodeEvaluationRule):
 class CodeDoesNotContain(base.CodeEvaluationRule):
     description = 'has code that does not contain {{x|CodeString}}'
 
+class OutputContains(base.CodeEvaluationRule):
+    description = 'has output that contains {{x|CodeString}}'
 
 class OutputEquals(base.CodeEvaluationRule):
     description = 'has output equal to {{x|CodeString}}'

Add an OutputContains rule to the CodeRepl interaction.
We've had a request to add an OutputContains rule to the CodeRepl interaction.

The use case is as follows: the student will type in the body of a function, and their code will be checked by calling the function on several inputs and printing the results. We don't want to stop the student from printing their own stuff from the function first, though, hence the idea of checking to see whether a substring of the student's output matches the expected output.

Note that this is a straightforward starter project. The files to modify are extensions/interactions/CodeRepl/CodeRepl.js (see codeReplRulesService) and the corresponding test suite in extensions/interactions/CodeRepl/CodeReplRulesServiceSpec.js.

/cc @anuzis 

