Repository: oppia/oppia

Issue:
Move remaining directives to directly reference templates using templateURLs.
(This follows on from #2700.)

Angular directives should directly reference their templates rather than using IDs and the template cache, in order to allow direct caching of HTML templates by the browser. Each directive should end up in its own file -- so if a file has multiple directives in it, it should be split, and the filename should match that of the directive. All directive template files should end in _directive.html, and all modal directive files should end in _modal_directive.html.

After this is done, we should also add a custom lint check that verifies that "script ng-template=" is not used throughout the codebase, except perhaps in a rare set of whitelisted cases (eg extensions). 

Here is the list of files that need to be updated:
- [x] components/create_button/create_activity_button_directive.html:
    1: <script type="text/ng-template" id="components/createActivityButton">
- [x] pages/collection_editor/collection_editor_navbar_directive.html:
    1: <script type="text/ng-template" id="inline/collection_editor_navbar_directive">
- [x] pages/creator_dashboard/create_activity_modal_directive.html:
    1: <script type="text/ng-template" id="modals/createActivity">
- [x] pages/exploration_editor/exploration_save_and_publish_buttons_directive.html:
   67: <script type="text/ng-template" id="modals/confirmDiscardChanges">    
- [x] pages/exploration_editor/editor_tab/exploration_graph.html:
   13:   <script type="text/ng-template" id="modals/stateGraph">
   30:   <script type="text/ng-template" id="modals/deleteState">
- [x] pages/exploration_editor/editor_tab/state_editor.html:
   40: <script type="text/ng-template" id="teaching/trainingPanel">
  116: <script type="text/ng-template" id="modals/trainUnresolvedAnswer">
- [x] pages/exploration_editor/editor_tab/state_editor_hints.html:
   64: <script type="text/ng-template" id="modals/addHint">
   88: <script type="text/ng-template" id="modals/deleteHint">
- [x] pages/exploration_editor/editor_tab/state_editor_interaction.html:
   33: <script type="text/ng-template" id="modals/customizeInteraction">
  105: <script type="text/ng-template" id="modals/deleteInteraction">
  126: <script type="text/ng-template" id="teaching/testInteractionPanel">
- [x] pages/exploration_editor/editor_tab/state_editor_responses.html:
  113:   <script type="text/ng-template" id="modals/teachOppia">
  172: <script type="text/ng-template" id="modals/addAnswerGroup">
  224: <script type="text/ng-template" id="modals/deleteAnswerGroup">
- [x] pages/exploration_editor/feedback_tab/feedback_tab.html:
  152: <script type="text/ng-template" id="modals/editorFeedbackCreateThread">
  168: <script type="text/ng-template" id="modals/editorViewSuggestion">
- [x] pages/exploration_editor/history_tab/history_tab.html:
   49: <script type="text/ng-template" id="modals/revertExploration">
- [x] pages/exploration_editor/settings_tab/settings_tab.html:
  440: <script type="text/ng-template" id="modals/transferExplorationOwnership">
  472: <script type="text/ng-template" id="modals/takeModeratorAction">
  504: <script type="text/ng-template" id="modals/deleteExploration">
  519: <script type="text/ng-template" id="modals/previewSummaryTile">
- [x] pages/exploration_editor/statistics_tab/statistics_tab.html:
   37: <script type="text/ng-template" id="modals/stateStats">
- [x] pages/exploration_player/exploration_player.html:
   51:   <script type="text/ng-template" id="modals/learnerSuggestionSubmitted">
   66:   <script type="text/ng-template" id="modals/learnerViewSuggestion">
- [ ] pages/exploration_player/feedback_popup_container_directive.html:
    1: <script type="text/ng-template" id="popover/feedback">
- [ ] pages/exploration_player/input_response_pair_directive.html:
   73: <script type="text/ng-template" id="popover/answer">
- [x] pages/learner_dashboard/learner_dashboard.html:
  755:   <script type="text/ng-template" id="modals/learnerViewSuggestion">
- [x] pages/preferences/preferences.html:
  264:       <script type="text/ng-template" id="modals/editProfilePicture">
- [x] pages/signup/signup.html:
  110:   <script type="text/ng-template" id="modals/licenseExplanation">
- [x] pages/tests/form_builder_test_page.html:
   99:   <script type="text/ng-template" id="formOverlay/entryPoint">
- [x] Add a custom lint check that verifies that "script ng-template=" is not used throughout the codebase, except perhaps in a rare set of whitelisted cases (eg extensions).


Discussion:
__naming issues__

- pages/creator_dashboard/create_activity_modal_directive.html 1:-> It's used in CreateActivityButtonDirective.js and create_activity_button_directive.html already exists.

- /exploration_save_and_publish_buttons_directive.html 67:-> used in ExplorationSaveService.js, what should be the naming conventions.

- /exploration_graph.html 30: -> used in EditorServices.js

- /editor_tab/state_editor.html 40:, 116: -> both are used in StateEditor.js


can we specify naming convention for these in more general sense. 

Not every `ng-template` mentioned is used in directives. Some are used in controllers and some are used in services. Also, there are cases where more than one are used in same file.
Ah -- let me clarify, sorry. Each directive _template_ should end up in its own file -- so if an _HTML_ file has multiple directives in it, it should be split, and the filename should match that of the directive. Does this help?

> - pages/creator_dashboard/create_activity_modal_directive.html 1:-> It's used in CreateActivityButtonDirective.js and create_activity_button_directive.html already exists.

This could still be create_activity_modal_directive.html, and the templateUrl in the $modal.open in CreateActivityButtonDirective.js will refer to the UrlInterpolationService-parsed version of pages/creator_dashboard/create_activity_modal_directive.html.

> /exploration_save_and_publish_buttons_directive.html 67:-> used in ExplorationSaveService.js, what should be the naming conventions.

Pull this out into confirm_discard_changes_modal_directive.html and treat it similarly to the previous bullet point.

> /exploration_graph.html 30: -> used in EditorServices.js

Same thing, pull it out into delete_state_modal_directive.html

> /editor_tab/state_editor.html 40:, 116: -> both are used in StateEditor.js

And these become training_panel_directive.html and train_unresolved_answer_modal_directive.html, similarly.

Does that help? Also, please feel free to tackle this issue in multiple PRs if it's easier and you'd like confirmation on earlier steps before tackling later ones.
Thanks, I think I am clear with the naming now.

I am thinking of breaking it in two but let's see if I face some problem I'll put up a WIP PR.

pages/exploration_player/feedback_popup_container_directive.html

pages/exploration_player/input_response_pair_directive.html

Both of the above templates are used in `popover-template` tag in the html files directly. Filenames can not be directly given in this tag as they won't compile. So, I think ng-template tag would be needed at these places.
OK, that's fine -- thanks, @1995YogeshSharma! Let's whitelist it in the final check mentioned above.

Resolving patch:
diff --git a/scripts/pre_commit_linter.py b/scripts/pre_commit_linter.py
--- a/scripts/pre_commit_linter.py
+++ b/scripts/pre_commit_linter.py
@@ -102,12 +102,49 @@
         'regexp': r"\b(ddescribe|fdescribe)\(",
         'message': "In tests, please use 'describe' instead of 'ddescribe'"
                    "or 'fdescribe'",
-        'excluded_files': ()
+        'excluded_files': (),
+        'excluded_dirs': ()
     },
     {
         'regexp': r"\b(iit|fit)\(",
         'message': "In tests, please use 'it' instead of 'iit' or 'fit'",
-        'excluded_files': ()
+        'excluded_files': (),
+        'excluded_dirs': ()
+    },
+    {
+        'regexp': r"templateUrl: \'",
+        'message': "The directives must be directly referenced.",
+        'excluded_files': (
+            'core/templates/dev/head/pages/exploration_player/'
+            'FeedbackPopupDirective.js'
+        ),
+        'excluded_dirs': (
+            'extensions/answer_summarizers/',
+            'extensions/classifiers/',
+            'extensions/dependencies/',
+            'extensions/objects/',
+            'extensions/value_generators/',
+            'extensions/visualizations/')
+    }
+]
+
+BAD_PATTERNS_HTML_REGEXP = [
+    {
+        'regexp': r"text\/ng-template",
+        'message': "The directives must be directly referenced.",
+        'excluded_files': (
+            'core/templates/dev/head/pages/exploration_player/'
+            'feedback_popup_container_directive.html',
+            'core/templates/dev/head/pages/exploration_player/'
+            'input_response_pair_directive.html'
+        ),
+        'excluded_dirs': (
+            'extensions/answer_summarizers/',
+            'extensions/classifiers/',
+            'extensions/dependencies/',
+            'extensions/objects/',
+            'extensions/value_generators/',
+            'extensions/visualizations/')
     }
 ]
 
@@ -124,6 +161,7 @@
     'integrations_dev/*', '*.svg', '*.png', '*.zip', '*.ico', '*.jpg',
     '*.min.js', 'assets/scripts/*', 'core/tests/data/*', '*.mp3')
 
+
 if not os.getcwd().endswith('oppia'):
     print ''
     print 'ERROR    Please run this script from the oppia root directory.'
@@ -505,6 +543,33 @@ def _check_newline_character(all_files):
     return summary_messages
 
 
+def _check_bad_pattern_in_file(filename, content, pattern):
+    """Detects whether the given pattern is present in the file.
+
+    Args:
+        filename: str. Name of the file.
+        content: str. Contents of the file.
+        pattern: dict ( regexp(regex pattern) : pattern to match,
+            message(str) : message to show if pattern matches,
+            excluded_files(tuple(str)) : files to be excluded from matching,
+            excluded_dirs(tuple(str)) : directories to be excluded from
+                matching). 
+            Object containing details for the pattern to be checked.
+
+    Returns:
+        bool. True if there is bad pattern else false.
+    """
+    regexp = pattern['regexp']
+    if not (any(filename.startswith(excluded_dir)
+                for excluded_dir in pattern['excluded_dirs'])
+            or filename in pattern['excluded_files']):
+        if re.search(regexp, content):
+            print '%s --> %s' % (
+                filename, pattern['message'])
+            return True
+    return False
+
+
 def _check_bad_patterns(all_files):
     """This function is used for detecting bad patterns.
     """
@@ -529,16 +594,19 @@ def _check_bad_patterns(all_files):
                     print '%s --> %s' % (
                         filename, BAD_PATTERNS[pattern]['message'])
                     total_error_count += 1
+
             if filename.endswith('.js'):
                 for regexp in BAD_PATTERNS_JS_REGEXP:
-                    regexp_pattern = regexp['regexp']
-                    if filename not in regexp['excluded_files']:
-                        if re.search(regexp_pattern, content):
-                            failed = True
-                            print '%s --> %s' % (
-                                filename,
-                                regexp['message'])
-                            total_error_count += 1
+                    if _check_bad_pattern_in_file(filename, content, regexp):
+                        failed = True
+                        total_error_count += 1
+    
+            if filename.endswith('.html'):
+                for regexp in BAD_PATTERNS_HTML_REGEXP:
+                    if _check_bad_pattern_in_file(filename, content, regexp):
+                        failed = True
+                        total_error_count += 1
+
             if filename == 'app.yaml':
                 for pattern in BAD_PATTERNS_APP_YAML:
                     if pattern in content:
@@ -572,7 +640,8 @@ def main():
     newline_messages = _check_newline_character(all_files)
     linter_messages = _pre_commit_linter(all_files)
     pattern_messages = _check_bad_patterns(all_files)
-    all_messages = linter_messages + newline_messages + pattern_messages
+    all_messages = (
+        linter_messages + newline_messages + pattern_messages)
     if any([message.startswith(_MESSAGE_TYPE_FAILED) for message in
             all_messages]):
         sys.exit(1)


Test patch: