Repository: oppia/oppia

Issue:
Remove remaining parts of Jinja
After #9294 is merged the only remaining parts of Jinja will be the parts that are used to evaluate objects and properties. 

All the remaining code related to Jinja can be found in [jinja_utils.py](https://github.com/oppia/oppia/blob/develop/jinja_utils.py). We should get rid of it and replace it with our own solution.


Discussion:
Update: #9294 is merged!
Going through the code. The only place we use jinja is `extensions/value_generators/models/generators.py` in the **Copier** generator.

When the param `parse_with_jinja` is True, `jinja_utils` method `evaluate_object` is called. When I see the tests of the Copier generator most of the values passed are withing '{{}}'. For eg

- `generator.generate_value({'a': 'b'}, **{'value': '{{a}}', 'parse_with_jinja': True}), 'b')`
- `dependent_pc = param_domain.ParamChange('b', 'Copier', {'value': '{{a}}', 'parse_with_jinja': True})`

Front-end example:
```
customizationArgs: {
  parse_with_jinja: true,
  value: '{{ParamChange2}}'
},
```

I also see this Code generator in `data/explorations/binary_search/The Lazy Magician.yaml`

eg:
```
- customization_args:
        parse_with_jinja: true
        value: '{{pow(2,floor(log(rangeSize,2)))-1}}'
      generator_id: Copier
      name: largestValidRange
```

So in order to replace this `evaluate_object` from jinja_utils, Shall I consider the values to be are enclosed in double curly brackets and the string inside be a python statement.
My doubt was, should I do an eval of the python statement passed in the double curly brackets with the context passed. Or just replace the variable name in the python statement with the corresponding value of the context passed to the function?

@vojtechjelinek @seanlip PTAL!


@aasiffaizal Can you investigate what the effect of doing jinja_utils.evaluate_object() is on the various possible values and provide a table of inputs and outputs here? Then we can advise better.

Thanks!
Input | Output
----- | -------
`value = '{{a}}', context = {'a': 1}`| 1
`value = 'a{{a}}', context = {'a': 1}`| a1
`value = '{{a}}', context = {'a': 'b', 'd':'c'}`|b
`value = '{{a+b}}', context = {'a': 1, 'b':3}`|4
`value = '{{a+b}}', context = {'a': 1, 'b':'hello'}`|[CONTENT PARSING ERROR]
`value = '{{a}}', context = {}`| [CONTENT PARSING ERROR]

This is the input-output table for `jinja_utils.evaluate_object`. I can see that inside the double curly brackets, it's a python statement. It's throwing a parsing error when the python statement is incorrect or when all the variables are not passed inside it. A similar param change function is found in the front end in `core/templates/pages/exploration-player-page/services/exploration-engine-service.ts`.

```ts
if (pc.generatorId === 'Copier') {
  if (!pc.customizationArgs.parse_with_jinja) {
    newParams[pc.name] = pc.customizationArgs.value;
  } else {
    var paramValue = ExpressionInterpolationService.processUnicode(
      pc.customizationArgs.value, [newParams].concat(envs));
    if (paramValue === null) {
      return false;
    }
    newParams[pc.name] = paramValue;
  }
} 
```

`ExpressionInterpolationService.processUnicode` method does the same evaluation as `jinja_utils.evaluate_object`.
```ts
processUnicode(
    sourceUnicode: string, envs: Record<string, string>[]): string {
  try {
    return sourceUnicode.replace(/{{([^}]*)}}/g, (match, p1)=> {
      // TODO(sll): Remove the call to $filter once we have a
      // custom UI for entering expressions. It is only needed because
      // expressions are currently input inline via the RTE.
      return this.expressionEvaluatorService.evaluateExpression(
        convertHtmlToUnicode(p1), envs) as string;
    });
  } catch (e) {
    if ((e instanceof this.expressionParserService.SyntaxError) ||
        (e instanceof this.expressionSyntaxTreeService.ExpressionError)) {
      return null;
    }
    throw e;
  }
}
```

So shall I replace the `jinja_utils.evaluate_object`by a function that does the following:

1. Find all the python statements inside {{}} using a regex that the frontend uses. (considering that there can be multiple python statements)
2. using the abstract syntax tree, eval the statement.
3. Throw errors if the python statement has errors or when sufficient variables aren't passed to eval the python statement.

@vojtechjelinek @seanlip PTAL!

Yep, your approach seems fine, I didn't know we have a code for this in FE too.  
Yeah, I found that while analyzing what are the values to be evaluated in the FE. I'll go ahead with this approach and raise a PR soon. :)
I think eval is a problem, actually. It's not safe in python and has security holes.

In which cases do we actually need the result of eval in the backend? If it's not really needed then maybe we can skip evaluation in the backend. Otherwise we'd need to write our own evaluator.
Seems like it's only in the Copier generator (generate_value). Given that we don't properly support params any more, I think it's fine to just parse the simple cases and then have some sort of error for the non-simple cases.
@seanlip Yeah for FE, we have basically implemented a custom evaluator in `core/templates/expressions/expression/expression-syntax-tree.service.ts`. So I was planning to implement something similar so that the evaluation is safe.
```ts
private system: {[name: string]: SystemEnv} = {
  '+': {
    eval: (args: string[]): number => {
      this.verifyNumArgs(args, 1, 2);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return numericArgs.length === 1 ?
        numericArgs[0] :
        numericArgs[0] + numericArgs[1];
    }
  },

  '-': {
    eval: (args: string[]): number => {
      this.verifyNumArgs(args, 1, 2);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return numericArgs.length === 1 ?
        -numericArgs[0] :
        numericArgs[0] - numericArgs[1];
    }
  },

  '*': {
    eval: (args: string[]): number => {
      this.verifyNumArgs(args, 2);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return numericArgs[0] * numericArgs[1];
    }
  },

  '/': {
    eval: (args: string[]): number => {
      this.verifyNumArgs(args, 2);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return numericArgs[0] / numericArgs[1];
    }
  },

  '%': {
    eval: (args: string[]): number => {
      this.verifyNumArgs(args, 2);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return numericArgs[0] % numericArgs[1];
    }
  },

  '<=': {
    eval: (args: string[]): boolean => {
      this.verifyNumArgs(args, 2);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return numericArgs[0] <= numericArgs[1];
    }
  },

  '>=': {
    eval: (args: string[]): boolean => {
      this.verifyNumArgs(args, 2);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return numericArgs[0] >= numericArgs[1];
    }
  },

  '<': {
    eval: (args: string[]): boolean => {
      this.verifyNumArgs(args, 2);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return numericArgs[0] < numericArgs[1];
    }
  },

  '>': {
    eval: (args: string[]): boolean => {
      this.verifyNumArgs(args, 2);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return numericArgs[0] > numericArgs[1];
    }
  },

  '!': {
    eval: (args: string[]): boolean => {
      this.verifyNumArgs(args, 1);
      return !args[0];
    }
  },

  '==': {
    eval: (args: string[]): boolean => {
      this.verifyNumArgs(args, 2);
      return args[0] === args[1];
    }
  },

  '!=': {
    eval: (args: string[]): boolean => {
      this.verifyNumArgs(args, 2);
      return args[0] !== args[1];
    }
  },

  '&&': {
    eval: (args: string[]): boolean => {
      this.verifyNumArgs(args, 2);
      return Boolean(args[0] && args[1]);
    }
  },

  '||': {
    eval: (args: string[]): boolean => {
      this.verifyNumArgs(args, 2);
      return Boolean(args[0] || args[1]);
    }
  },

  // NOTE TO DEVELOPERS: Removing the quotation marks from the following keys
  // causes issues with minification when running the deployment scripts.
  'if': { // eslint-disable-line quote-props
    eval: (args: string[]): string => {
      this.verifyNumArgs(args, 3);
      return args[0] ? args[1] : args[2];
    }
  },

  'floor': { // eslint-disable-line quote-props
    eval: (args: string[]): number => {
      this.verifyNumArgs(args, 1);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return Math.floor(numericArgs[0]);
    }
  },

  'pow': { // eslint-disable-line quote-props
    eval: (args: string[]): number => {
      this.verifyNumArgs(args, 2);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return Math.pow(numericArgs[0], numericArgs[1]);
    }
  },

  'log': { // eslint-disable-line quote-props
    eval: (args: string[]): number => {
      this.verifyNumArgs(args, 2);
      const numericArgs = this.coerceAllArgsToNumber(args);
      const preciseAns = Math.log(numericArgs[0]) / Math.log(numericArgs[1]);
      // We round answers to 9 decimal places, so that we don't run into
      // issues like log(9, 3) = 2.0000000000004.
      return Math.round(preciseAns * Math.pow(10, 9)) / Math.pow(10, 9);
    }
  },

  'abs': { // eslint-disable-line quote-props
    eval: (args: string[]): number => {
      this.verifyNumArgs(args, 1);
      const numericArgs = this.coerceAllArgsToNumber(args);
      return Math.abs(numericArgs[0]);
    }
  }
};
```

So by the simple cases, you mean the first 2 cases in the IO table which I mentioned above? or should we add certain more operations such as binary operation and conditional statement? Let me know what all cases I should cover. :)


I would suggest: (a) do the first three cases in the IO table, and, separately, (b) write a one-off job that Vojta or I can run on backup data to see what other cases actually exist in prod. That will both minimize work and tell us what the diff from reality is. How does that sound?
Sounds great. Will start implementing the first three cases. I will need a little more context on the one-off job. I mean what it should and what it should analyze. 
It should look at all usages of the Copier generator in each exploration and output all cases (together with exp ID + state name identification) where the string to be evaluated isn't one of the forms we're already planning to handle. Does that make sense?
Yeah got a better idea. Any sample one-off job that I can refer to see how it is written? and the generator will be a part of the `param_changes` field right?

Please talk to @vojtechjelinek or @rohitkatlaa, they can guide you on this. Thanks! 
Looked more into where the `jinja_utils.evaluate_object` is called. The below path is how it is called.
 -> `generate_value` from `extensions/value_generators/models/generators.py`
 -> `_get_value` from `core/domain/param_domain`
 -> `get_normalized_value` from `core/domain/param_domain`

`get_normalized_value` is used only by `get_updated_param_dict` from `core/tests/test_utils.py`. I am assuming all the current param_change is done in the FE itself. In the BE only `test_utils.py` is using it. Since it is only used by only `test_utils.py` we could do either of the given below solution right?

- Remove parse_jinja completely from the codebase since we aren't using it anywhere except tests
- Support only the first 3 i/o cases from the above table since only those inputs can be seen in tests.

PTAL @vojtechjelinek @seanlip. Thanks :)
@aasiffaizal Hmmm, I understand that the `get_normalized_value` is only used from _test_utils.py_, but what about the other two (`_get_value` and `generate_value`)? It seems that these are used too and that we pass data from the datastore to them, right?
@vojtechjelinek If I am not wrong, `generate_value` is only used in `_get_value` and a few other tests. and `_get_value` is only called in `get_normalized_value`.
@aasiffaizal Oh I see that you're providing a stack trace and not a list of files. Then you're right and I think your approach seems correct, although I think that if we actually only use this in tests we might be able to get rid of it entirely.
@vojtechjelinek Yeah I was providing a stack trace :D. So shall I move forward in removing just the parse_with_jinja part or the complete generator file?
Yep, fine by me.

Resolving patch:
diff --git a/extensions/value_generators/models/generators.py b/extensions/value_generators/models/generators.py
--- a/extensions/value_generators/models/generators.py
+++ b/extensions/value_generators/models/generators.py
@@ -22,7 +22,6 @@
 import copy
 
 from core.domain import value_generators_domain
-import jinja_utils
 import utils
 
 
@@ -31,23 +30,22 @@ class Copier(value_generators_domain.BaseValueGenerator):
 
     default_value = ''
 
-    def generate_value(self, context_params, value, parse_with_jinja=False):
+    def generate_value(
+            self, unused_context_params, value, parse_with_jinja=False):  # pylint: disable=unused-argument
         """Returns a copy of the input value.
 
-        If parse_with_jinja is True, strings within the input value are treated
-        as templates and parsed against context_params. The output will be a
-        unicode string.
+        Args:
+            unused_context_params: dict. Context params parsed with input
+                value which is treated as a template. Not used.
+            value: str. Value whose copy should be returned.
+            parse_with_jinja: bool. It is a part of ParamChange object.
+                The parsing of input value against context_params
+                based on parse_with_jinja is done in the FE. Not used.
 
-        If parse_with_jinja is False, the input value is copied and returned
-        without changing its type.
+        Returns:
+            str. Copy of the input value.
         """
-        if context_params is None:
-            context_params = {}
-
-        if parse_with_jinja:
-            return jinja_utils.evaluate_object(value, context_params)
-        else:
-            return copy.deepcopy(value)
+        return copy.deepcopy(value)
 
 
 class RandomSelector(value_generators_domain.BaseValueGenerator):
diff --git a/jinja_utils.py b/jinja_utils.py
deleted file mode 100644
--- a/jinja_utils.py
+++ /dev/null
@@ -1,131 +0,0 @@
-# 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, 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.
-
-"""Jinja-related utilities."""
-
-from __future__ import absolute_import  # pylint: disable=import-only-modules
-from __future__ import unicode_literals  # pylint: disable=import-only-modules
-
-import copy
-import json
-import logging
-import math
-
-import jinja2
-from jinja2 import meta
-
-import python_utils  # isort:skip
-
-
-def _js_string_filter(value):
-    """Converts a value to a JSON string for use in JavaScript code.
-
-    Args:
-        value: *. The specified value to be coverted to JSON string.
-
-    Returns:
-        str. The resulting JSON string.
-    """
-    string = json.dumps(value)
-
-    replacements = [('\\', '\\\\'), ('"', '\\"'), ('\'', '\\\''),
-                    ('\n', '\\n'), ('\r', '\\r'), ('\b', '\\b'),
-                    ('<', '\\u003c'), ('>', '\\u003e'), ('&', '\\u0026')]
-
-    for replacement in replacements:
-        string = string.replace(replacement[0], replacement[1])
-    return jinja2.utils.Markup(string)
-
-
-def _log2_floor_filter(value):
-    """Returns the logarithm base 2 of the given value, rounded down.
-
-    Args:
-        value: int|float. The specified value.
-
-    Returns:
-        int. The rounded off value of logarithm base 2 of the given value.
-    """
-    return int(math.log(value, 2))
-
-
-JINJA_FILTERS = {
-    'is_list': lambda x: isinstance(x, list),
-    'is_dict': lambda x: isinstance(x, dict),
-    'js_string': _js_string_filter,
-    'log2_floor': _log2_floor_filter,
-}
-
-
-def parse_string(string, params, autoescape=True):
-    """Parses a string using Jinja templating.
-
-    Args:
-        string: str. The string to be parsed.
-        params: dict(str, *). The parameters to parse the string with.
-        autoescape: bool. Whether to enable autoescaping when parsing.
-
-    Returns:
-        str. The string parsed using Jinja templating. Returns an error string
-        in case of error in parsing.
-
-    Raises:
-        Exception. Unable to parse string with Jinja.
-    """
-    env = jinja2.Environment(autoescape=autoescape)
-
-    env.filters.update(JINJA_FILTERS)
-    try:
-        parsed_string = env.parse(string)
-    except jinja2.exceptions.TemplateError:
-        raise Exception('Unable to parse string with Jinja: %s' % string)
-
-    variables = meta.find_undeclared_variables(parsed_string)
-    if any([var not in params for var in variables]):
-        logging.info('Cannot parse %s fully using %s', string, params)
-
-    try:
-        return env.from_string(string).render(params)
-    except Exception:
-        logging.error(
-            'jinja_utils.parse_string() failed with args: %s, %s, %s' %
-            (string, params, autoescape))
-        return env.from_string('[CONTENT PARSING ERROR]').render({})
-
-
-def evaluate_object(obj, params):
-    """Returns a copy of `obj` after parsing strings in it using `params`.
-
-    Args:
-        obj: *. An object containing strings that need to be parsed.
-        params: dict(str, *). The parameters to parse the string with.
-
-    Returns:
-        *. The copy of `obj` after parsing strings in it.
-    """
-
-    if isinstance(obj, python_utils.BASESTRING):
-        return parse_string(obj, params)
-    elif isinstance(obj, list):
-        new_list = []
-        for item in obj:
-            new_list.append(evaluate_object(item, params))
-        return new_list
-    elif isinstance(obj, dict):
-        new_dict = {}
-        for key in obj:
-            new_dict[key] = evaluate_object(obj[key], params)
-        return new_dict
-    else:
-        return copy.deepcopy(obj)


Test patch:
diff --git a/core/controllers/reader_test.py b/core/controllers/reader_test.py
--- a/core/controllers/reader_test.py
+++ b/core/controllers/reader_test.py
@@ -402,54 +402,12 @@ def test_get_init_params(self):
         """Test the get_init_params() method."""
         independent_pc = param_domain.ParamChange(
             'a', 'Copier', {'value': 'firstValue', 'parse_with_jinja': False})
-        dependent_pc = param_domain.ParamChange(
-            'b', 'Copier', {'value': '{{a}}', 'parse_with_jinja': True})
-
-        exp_param_specs = {
-            'a': param_domain.ParamSpec('UnicodeString'),
-            'b': param_domain.ParamSpec('UnicodeString'),
-        }
-        new_params = self.get_updated_param_dict(
-            {}, [independent_pc, dependent_pc], exp_param_specs)
-        self.assertEqual(new_params, {'a': 'firstValue', 'b': 'firstValue'})
-
-        # Jinja string evaluation fails gracefully on dependencies that do not
-        # exist.
-        new_params = self.get_updated_param_dict(
-            {}, [dependent_pc, independent_pc], exp_param_specs)
-        self.assertEqual(new_params, {'a': 'firstValue', 'b': ''})
-
-    def test_update_learner_params(self):
-        """Test the update_learner_params() method."""
-        independent_pc = param_domain.ParamChange(
-            'a', 'Copier', {'value': 'firstValue', 'parse_with_jinja': False})
-        dependent_pc = param_domain.ParamChange(
-            'b', 'Copier', {'value': '{{a}}', 'parse_with_jinja': True})
-
         exp_param_specs = {
             'a': param_domain.ParamSpec('UnicodeString'),
-            'b': param_domain.ParamSpec('UnicodeString'),
         }
-
-        old_params = {}
-        new_params = self.get_updated_param_dict(
-            old_params, [independent_pc, dependent_pc], exp_param_specs)
-        self.assertEqual(new_params, {'a': 'firstValue', 'b': 'firstValue'})
-        self.assertEqual(old_params, {})
-
-        old_params = {'a': 'secondValue'}
-        new_params = self.get_updated_param_dict(
-            old_params, [dependent_pc], exp_param_specs)
-        self.assertEqual(new_params, {'a': 'secondValue', 'b': 'secondValue'})
-        self.assertEqual(old_params, {'a': 'secondValue'})
-
-        # Jinja string evaluation fails gracefully on dependencies that do not
-        # exist.
-        old_params = {}
         new_params = self.get_updated_param_dict(
-            old_params, [dependent_pc], exp_param_specs)
-        self.assertEqual(new_params, {'b': ''})
-        self.assertEqual(old_params, {})
+            {}, [independent_pc], exp_param_specs)
+        self.assertEqual(new_params, {'a': 'firstValue'})
 
 
 class RatingsIntegrationTests(test_utils.GenericTestBase):
diff --git a/extensions/value_generators/models/generators_test.py b/extensions/value_generators/models/generators_test.py
--- a/extensions/value_generators/models/generators_test.py
+++ b/extensions/value_generators/models/generators_test.py
@@ -28,15 +28,9 @@ class ValueGeneratorUnitTests(test_utils.GenericTestBase):
 
     def test_copier(self):
         generator = generators.Copier()
-        self.assertEqual(generator.generate_value({}, **{'value': 'a'}), 'a')
+        self.assertEqual(generator.generate_value(None, **{'value': 'a'}), 'a')
         self.assertEqual(generator.generate_value(
-            {}, **{'value': 'a', 'parse_with_jinja': False}), 'a')
-        self.assertEqual(generator.generate_value(
-            None, **{'value': 'a', 'parse_with_jinja': False}), 'a')
-        self.assertEqual(generator.generate_value(
-            {}, **{'value': '{{a}}', 'parse_with_jinja': False}), '{{a}}')
-        self.assertEqual(generator.generate_value(
-            {'a': 'b'}, **{'value': '{{a}}', 'parse_with_jinja': True}), 'b')
+            None, **{'value': '{{a}}'}), '{{a}}')
         self.assertIn(
             'init-args="initArgs" value="customizationArgs.value"',
             generator.get_html_template())
diff --git a/jinja_utils_test.py b/jinja_utils_test.py
deleted file mode 100644
--- a/jinja_utils_test.py
+++ /dev/null
@@ -1,138 +0,0 @@
-# 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, 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.
-
-"""Unit tests for jinja_utils.py."""
-
-from __future__ import absolute_import  # pylint: disable=import-only-modules
-from __future__ import unicode_literals  # pylint: disable=import-only-modules
-
-from core.tests import test_utils
-import jinja_utils
-import python_utils
-
-
-class JinjaUtilsUnitTests(test_utils.GenericTestBase):
-
-    def test_js_string_filter(self):
-        """Test js_string filter."""
-        expected_values = [
-            ('a', '\\"a\\"'),
-            (2, '2'),
-            (5.5, '5.5'),
-            ('\'', '\\"\\\'\\"'),
-            (u'¡Hola!', '\\"\\\\u00a1Hola!\\"'),
-            (['a', '¡Hola!', 2], '[\\"a\\", \\"\\\\u00a1Hola!\\", 2]'),
-            ({'a': 4, '¡Hola!': 2}, '{\\"a\\": 4, \\"\\\\u00a1Hola!\\": 2}'),
-            ('', '\\"\\"'),
-            (None, 'null'),
-            ([
-                'a', {'b': 'c', 'd': ['e', None]}],
-             '[\\"a\\", {\\"b\\": \\"c\\", \\"d\\": [\\"e\\", null]}]')
-        ]
-
-        for tup in expected_values:
-            self.assertEqual(jinja_utils.JINJA_FILTERS['js_string'](
-                tup[0]), tup[1])
-
-    def test_parse_string(self):
-        parsed_str = jinja_utils.parse_string('{{test}}', {'test': 'hi'})
-        self.assertEqual(parsed_str, 'hi')
-
-        # Some parameters are missing.
-        parsed_str = jinja_utils.parse_string(
-            '{{test}} and {{test2}}', {'test2': 'hi'})
-        self.assertEqual(parsed_str, ' and hi')
-
-        # All parameters are missing.
-        parsed_str = jinja_utils.parse_string('{{test}} and {{test2}}', {})
-        self.assertEqual(parsed_str, ' and ')
-
-        # The string has no parameters.
-        parsed_str = jinja_utils.parse_string('no params', {'param': 'hi'})
-        self.assertEqual(parsed_str, 'no params')
-
-        # Integer parameters are used.
-        parsed_str = jinja_utils.parse_string('int {{i}}', {'i': 2})
-        self.assertEqual(parsed_str, 'int 2')
-
-        # Invalid input string is used.
-        with self.assertRaisesRegexp(
-            Exception,
-            'Unable to parse string with Jinja: {{'
-        ):
-            jinja_utils.parse_string('{{', {'a': 3, 'b': 0})
-
-        # Invalid expression is used.
-        parsed_str = jinja_utils.parse_string('{{ a/b }}', {'a': 1, 'b': 0})
-        self.assertEqual(
-            parsed_str, python_utils.UNICODE('[CONTENT PARSING ERROR]'))
-
-    def test_evaluate_object(self):
-        parsed_object = jinja_utils.evaluate_object('abc', {})
-        self.assertEqual(parsed_object, 'abc')
-
-        parsed_object = jinja_utils.evaluate_object('{{ab}}', {'ab': 'c'})
-        self.assertEqual(parsed_object, 'c')
-
-        parsed_object = jinja_utils.evaluate_object('abc{{ab}}', {'ab': 'c'})
-        self.assertEqual(parsed_object, 'abcc')
-
-        parsed_object = jinja_utils.evaluate_object(
-            ['a', '{{a}}', 'a{{a}}'], {'a': 'b'})
-        self.assertEqual(parsed_object, ['a', 'b', 'ab'])
-
-        parsed_object = jinja_utils.evaluate_object({}, {})
-        self.assertEqual(parsed_object, {})
-
-        parsed_object = jinja_utils.evaluate_object({}, {'a': 'b'})
-        self.assertEqual(parsed_object, {})
-
-        parsed_object = jinja_utils.evaluate_object({'a': 'b'}, {})
-        self.assertEqual(parsed_object, {'a': 'b'})
-
-        parsed_object = jinja_utils.evaluate_object(
-            {'a': 'a{{b}}'}, {'b': 'c'})
-        self.assertEqual(parsed_object, {'a': 'ac'})
-
-        parsed_object = jinja_utils.evaluate_object({'a': '{{b}}'}, {'b': 3})
-        self.assertEqual(parsed_object, {'a': '3'})
-
-        parsed_object = jinja_utils.evaluate_object({'a': '{{b}}'}, {'b': 'c'})
-        self.assertEqual(parsed_object, {'a': 'c'})
-
-        # Failure cases should be handled gracefully.
-        parsed_object = jinja_utils.evaluate_object('{{c}}', {})
-        self.assertEqual(parsed_object, '')
-
-        parsed_object = jinja_utils.evaluate_object('{{c}}', {'a': 'b'})
-        self.assertEqual(parsed_object, '')
-
-        # Test that the original dictionary is unchanged.
-        orig_dict = {'a': '{{b}}'}
-        parsed_dict = jinja_utils.evaluate_object(orig_dict, {'b': 'c'})
-        self.assertEqual(orig_dict, {'a': '{{b}}'})
-        self.assertEqual(parsed_dict, {'a': 'c'})
-
-        # Test that int type input is used.
-        parsed_object = jinja_utils.evaluate_object(34, {})
-        self.assertEqual(parsed_object, 34)
-
-    def test__log2_floor_filter(self):
-        log_value = jinja_utils.JINJA_FILTERS['log2_floor'](10)
-        self.assertEqual(log_value, 3)
-
-        log_value = jinja_utils.JINJA_FILTERS['log2_floor'](0.0001)
-        self.assertEqual(log_value, -13)
diff --git a/scripts/backend_test_shards.json b/scripts/backend_test_shards.json
--- a/scripts/backend_test_shards.json
+++ b/scripts/backend_test_shards.json
@@ -248,7 +248,6 @@
         "core.storage.subtopic.gae_models_test",
         "core.domain.user_domain_test",
         "core.storage.opportunity.gae_models_test",
-        "jinja_utils_test",
         "scripts.build_test",
         "scripts.install_third_party_libs_test",
         "core.storage.statistics.gae_models_test",