We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[Contributor Dashboard] Ensure try/catch blocks in suggestion.py print useful stack traces.
Specifically we want to ensure the try/catch blocks `ReviewableSuggestionsHandler` and `UserSubmittedSuggestionsHandler` print useful stack traces for debugging. Because Python omits any part of the stack trace "below" the top-level exception in a try/catch block, it is difficult to trace the origin of an exception during debugging. This cost a lot of time when we were debugging the contributor dashboard on a test server.

Some options include removing the try/catch block or using something similar to [`traceback.format_exc()`](https://stackoverflow.com/a/3702847) to print useful stack traces.
[Contributor Dashboard] Ensure try/catch blocks in suggestion.py print useful stack traces.
Specifically we want to ensure the try/catch blocks `ReviewableSuggestionsHandler` and `UserSubmittedSuggestionsHandler` print useful stack traces for debugging. Because Python omits any part of the stack trace "below" the top-level exception in a try/catch block, it is difficult to trace the origin of an exception during debugging. This cost a lot of time when we were debugging the contributor dashboard on a test server.

Some options include removing the try/catch block or using something similar to [`traceback.format_exc()`](https://stackoverflow.com/a/3702847) to print useful stack traces.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `core/controllers/suggestion.py`
Content:
```
1 # coding: utf-8
2 #
3 # Copyright 2018 The Oppia Authors. All Rights Reserved.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 #      http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS-IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 
17 """Controllers for suggestions."""
18 
19 from __future__ import absolute_import  # pylint: disable=import-only-modules
20 from __future__ import unicode_literals  # pylint: disable=import-only-modules
21 
22 from core.controllers import acl_decorators
23 from core.controllers import base
24 from core.domain import opportunity_services
25 from core.domain import skill_fetchers
26 from core.domain import suggestion_services
27 from core.platform import models
28 import feconf
29 import utils
30 
31 (suggestion_models,) = models.Registry.import_models([models.NAMES.suggestion])
32 
33 
34 def _require_valid_suggestion_and_target_types(target_type, suggestion_type):
35     """Checks whether the given target_type and suggestion_type are valid.
36 
37     Args:
38         target_type: str. The type of the suggestion target.
39         suggestion_type: str. The type of the suggestion.
40 
41     Raises:
42         InvalidInputException: If the given target_type of suggestion_type are
43             invalid.
44     """
45     if target_type not in suggestion_models.TARGET_TYPE_CHOICES:
46         raise utils.InvalidInputException(
47             'Invalid target_type: %s' % target_type)
48 
49     if suggestion_type not in suggestion_models.SUGGESTION_TYPE_CHOICES:
50         raise utils.InvalidInputException(
51             'Invalid suggestion_type: %s' % suggestion_type)
52 
53 
54 def _get_target_id_to_exploration_opportunity_dict(suggestions):
55     """Returns a dict of target_id to exploration opportunity summary dict.
56 
57     Args:
58         suggestions: list(BaseSuggestion). A list of suggestions to retrieve
59             opportunity dicts.
60 
61     Returns:
62         dict. Dict mapping target_id to corresponding exploration opportunity
63         summary dict.
64     """
65     target_ids = set([s.target_id for s in suggestions])
66     opportunities = (
67         opportunity_services.get_exploration_opportunity_summaries_by_ids(
68             list(target_ids)))
69     return {opp.id: opp.to_dict() for opp in opportunities}
70 
71 
72 def _get_target_id_to_skill_opportunity_dict(suggestions):
73     """Returns a dict of target_id to skill opportunity summary dict.
74 
75     Args:
76         suggestions: list(BaseSuggestion). A list of suggestions to retrieve
77             opportunity dicts.
78 
79     Returns:
80         dict. Dict mapping target_id to corresponding skill opportunity dict.
81     """
82     target_ids = set([s.target_id for s in suggestions])
83     opportunities = (
84         opportunity_services.get_skill_opportunities_by_ids(list(target_ids)))
85     opportunity_skill_ids = [opp.id for opp in opportunities]
86     opportunity_id_to_skill = {
87         skill.id: skill
88         for skill in skill_fetchers.get_multi_skills(opportunity_skill_ids)
89     }
90     opportunity_id_to_opportunity = {}
91     for opp in opportunities:
92         opp_dict = opp.to_dict()
93         skill = opportunity_id_to_skill.get(opp.id)
94         if skill is not None:
95             opp_dict['skill_rubrics'] = [
96                 rubric.to_dict() for rubric in skill.rubrics]
97         opportunity_id_to_opportunity[opp.id] = opp_dict
98     return opportunity_id_to_opportunity
99 
100 
101 class SuggestionHandler(base.BaseHandler):
102     """"Handles operations relating to suggestions."""
103 
104     @acl_decorators.can_suggest_changes
105     def post(self):
106         try:
107             suggestion_services.create_suggestion(
108                 self.payload.get('suggestion_type'),
109                 self.payload.get('target_type'), self.payload.get('target_id'),
110                 self.payload.get('target_version_at_submission'),
111                 self.user_id, self.payload.get('change'),
112                 self.payload.get('description'))
113         except utils.ValidationError as e:
114             raise self.InvalidInputException(e)
115         self.render_json(self.values)
116 
117 
118 class SuggestionToExplorationActionHandler(base.BaseHandler):
119     """Handles actions performed on suggestions to explorations."""
120 
121     @acl_decorators.get_decorator_for_accepting_suggestion(
122         acl_decorators.can_edit_exploration)
123     def put(self, target_id, suggestion_id):
124         if (
125                 suggestion_id.split('.')[0] !=
126                 suggestion_models.TARGET_TYPE_EXPLORATION):
127             raise self.InvalidInputException('This handler allows actions only'
128                                              ' on suggestions to explorations.')
129 
130         if suggestion_id.split('.')[1] != target_id:
131             raise self.InvalidInputException('The exploration id provided does '
132                                              'not match the exploration id '
133                                              'present as part of the '
134                                              'suggestion_id')
135 
136         action = self.payload.get('action')
137         suggestion = suggestion_services.get_suggestion_by_id(suggestion_id)
138 
139         if suggestion.author_id == self.user_id:
140             raise self.UnauthorizedUserException('You cannot accept/reject your'
141                                                  ' own suggestion.')
142 
143         if action == suggestion_models.ACTION_TYPE_ACCEPT:
144             suggestion_services.accept_suggestion(
145                 suggestion, self.user_id, self.payload.get('commit_message'),
146                 self.payload.get('review_message'))
147         elif action == suggestion_models.ACTION_TYPE_REJECT:
148             suggestion_services.reject_suggestion(
149                 suggestion, self.user_id, self.payload.get('review_message'))
150         else:
151             raise self.InvalidInputException('Invalid action.')
152 
153         self.render_json(self.values)
154 
155 
156 class ResubmitSuggestionHandler(base.BaseHandler):
157     """Handler to reopen a rejected suggestion."""
158 
159     @acl_decorators.can_resubmit_suggestion
160     def put(self, suggestion_id):
161         suggestion = suggestion_services.get_suggestion_by_id(suggestion_id)
162         new_change = self.payload.get('change')
163         change_cls = type(suggestion.change)
164         change_object = change_cls(new_change)
165         suggestion.pre_update_validate(change_object)
166         suggestion.change = change_object
167         summary_message = self.payload.get('summary_message')
168         suggestion_services.resubmit_rejected_suggestion(
169             suggestion, summary_message, self.user_id)
170         self.render_json(self.values)
171 
172 
173 class SuggestionToSkillActionHandler(base.BaseHandler):
174     """Handles actions performed on suggestions to skills."""
175 
176     @acl_decorators.get_decorator_for_accepting_suggestion(
177         acl_decorators.can_edit_skill)
178     def put(self, target_id, suggestion_id):
179         if suggestion_id.split('.')[0] != suggestion_models.TARGET_TYPE_SKILL:
180             raise self.InvalidInputException(
181                 'This handler allows actions only on suggestions to skills.')
182 
183         if suggestion_id.split('.')[1] != target_id:
184             raise self.InvalidInputException(
185                 'The skill id provided does not match the skill id present as '
186                 'part of the suggestion_id')
187 
188         action = self.payload.get('action')
189         suggestion = suggestion_services.get_suggestion_by_id(suggestion_id)
190 
191         if action == suggestion_models.ACTION_TYPE_ACCEPT:
192             # Question suggestions do not use commit messages.
193             suggestion_services.accept_suggestion(
194                 suggestion, self.user_id, 'UNUSED_COMMIT_MESSAGE',
195                 self.payload.get('review_message'))
196         elif action == suggestion_models.ACTION_TYPE_REJECT:
197             suggestion_services.reject_suggestion(
198                 suggestion, self.user_id, self.payload.get('review_message'))
199         else:
200             raise self.InvalidInputException('Invalid action.')
201 
202         self.render_json(self.values)
203 
204 
205 class SuggestionsProviderHandler(base.BaseHandler):
206     """Provides suggestions for a user and given suggestion type."""
207 
208     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
209 
210     def _render_suggestions(self, target_type, suggestions):
211         """Renders retrieved suggestions.
212 
213         Args:
214             target_type: str. The suggestion type.
215             suggestions: list(BaseSuggestion). A list of suggestions to render.
216         """
217         if target_type == suggestion_models.TARGET_TYPE_EXPLORATION:
218             target_id_to_opportunity_dict = (
219                 _get_target_id_to_exploration_opportunity_dict(suggestions))
220             self.render_json({
221                 'suggestions': [s.to_dict() for s in suggestions],
222                 'target_id_to_opportunity_dict':
223                     target_id_to_opportunity_dict
224             })
225         elif target_type == suggestion_models.TARGET_TYPE_SKILL:
226             target_id_to_opportunity_dict = (
227                 _get_target_id_to_skill_opportunity_dict(suggestions))
228             self.render_json({
229                 'suggestions': [s.to_dict() for s in suggestions],
230                 'target_id_to_opportunity_dict':
231                     target_id_to_opportunity_dict
232             })
233         else:
234             self.render_json({})
235 
236 
237 class ReviewableSuggestionsHandler(SuggestionsProviderHandler):
238     """Provides all suggestions which can be reviewed by the user for a given
239     suggestion type.
240     """
241 
242     @acl_decorators.can_view_reviewable_suggestions
243     def get(self, target_type, suggestion_type):
244         """Handles GET requests."""
245         try:
246             _require_valid_suggestion_and_target_types(
247                 target_type, suggestion_type)
248             suggestions = suggestion_services.get_reviewable_suggestions(
249                 self.user_id, suggestion_type)
250             self._render_suggestions(target_type, suggestions)
251         except Exception as e:
252             raise self.InvalidInputException(e)
253 
254 
255 class UserSubmittedSuggestionsHandler(SuggestionsProviderHandler):
256     """Provides all suggestions which are submitted by the user for a given
257     suggestion type.
258     """
259 
260     @acl_decorators.can_suggest_changes
261     def get(self, target_type, suggestion_type):
262         """Handles GET requests."""
263         try:
264             _require_valid_suggestion_and_target_types(
265                 target_type, suggestion_type)
266             suggestions = suggestion_services.get_submitted_suggestions(
267                 self.user_id, suggestion_type)
268             self._render_suggestions(target_type, suggestions)
269         except Exception as e:
270             raise self.InvalidInputException(e)
271 
272 
273 class SuggestionListHandler(base.BaseHandler):
274     """Handles list operations on suggestions."""
275 
276     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
277 
278     @acl_decorators.open_access
279     def get(self):
280         # The query_fields_and_values variable is a list of tuples. The first
281         # element in each tuple is the field being queried and the second
282         # element is the value of the field being queried.
283         # request.GET.items() parses the params from the url into the above
284         # format. So in the url, the query should be passed as:
285         # ?field1=value1&field2=value2...fieldN=valueN.
286         query_fields_and_values = list(self.request.GET.items())
287 
288         for query in query_fields_and_values:
289             if query[0] not in suggestion_models.ALLOWED_QUERY_FIELDS:
290                 raise self.InvalidInputException(
291                     'Not allowed to query on field %s' % query[0])
292 
293         suggestions = suggestion_services.query_suggestions(
294             query_fields_and_values)
295 
296         self.values.update({'suggestions': [s.to_dict() for s in suggestions]})
297         self.render_json(self.values)
298
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/core/controllers/suggestion.py b/core/controllers/suggestion.py
--- a/core/controllers/suggestion.py
+++ b/core/controllers/suggestion.py
@@ -31,26 +31,6 @@
 (suggestion_models,) = models.Registry.import_models([models.NAMES.suggestion])
 
 
-def _require_valid_suggestion_and_target_types(target_type, suggestion_type):
-    """Checks whether the given target_type and suggestion_type are valid.
-
-    Args:
-        target_type: str. The type of the suggestion target.
-        suggestion_type: str. The type of the suggestion.
-
-    Raises:
-        InvalidInputException: If the given target_type of suggestion_type are
-            invalid.
-    """
-    if target_type not in suggestion_models.TARGET_TYPE_CHOICES:
-        raise utils.InvalidInputException(
-            'Invalid target_type: %s' % target_type)
-
-    if suggestion_type not in suggestion_models.SUGGESTION_TYPE_CHOICES:
-        raise utils.InvalidInputException(
-            'Invalid suggestion_type: %s' % suggestion_type)
-
-
 def _get_target_id_to_exploration_opportunity_dict(suggestions):
     """Returns a dict of target_id to exploration opportunity summary dict.
 
@@ -207,6 +187,26 @@
 
     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
 
+    def _require_valid_suggestion_and_target_types(
+            self, target_type, suggestion_type):
+        """Checks whether the given target_type and suggestion_type are valid.
+
+        Args:
+            target_type: str. The type of the suggestion target.
+            suggestion_type: str. The type of the suggestion.
+
+        Raises:
+            InvalidInputException: If the given target_type of suggestion_type
+                are invalid.
+        """
+        if target_type not in suggestion_models.TARGET_TYPE_CHOICES:
+            raise self.InvalidInputException(
+                'Invalid target_type: %s' % target_type)
+
+        if suggestion_type not in suggestion_models.SUGGESTION_TYPE_CHOICES:
+            raise self.InvalidInputException(
+                'Invalid suggestion_type: %s' % suggestion_type)
+
     def _render_suggestions(self, target_type, suggestions):
         """Renders retrieved suggestions.
 
@@ -242,14 +242,11 @@
     @acl_decorators.can_view_reviewable_suggestions
     def get(self, target_type, suggestion_type):
         """Handles GET requests."""
-        try:
-            _require_valid_suggestion_and_target_types(
-                target_type, suggestion_type)
-            suggestions = suggestion_services.get_reviewable_suggestions(
-                self.user_id, suggestion_type)
-            self._render_suggestions(target_type, suggestions)
-        except Exception as e:
-            raise self.InvalidInputException(e)
+        self._require_valid_suggestion_and_target_types(
+            target_type, suggestion_type)
+        suggestions = suggestion_services.get_reviewable_suggestions(
+            self.user_id, suggestion_type)
+        self._render_suggestions(target_type, suggestions)
 
 
 class UserSubmittedSuggestionsHandler(SuggestionsProviderHandler):
@@ -260,14 +257,11 @@
     @acl_decorators.can_suggest_changes
     def get(self, target_type, suggestion_type):
         """Handles GET requests."""
-        try:
-            _require_valid_suggestion_and_target_types(
-                target_type, suggestion_type)
-            suggestions = suggestion_services.get_submitted_suggestions(
-                self.user_id, suggestion_type)
-            self._render_suggestions(target_type, suggestions)
-        except Exception as e:
-            raise self.InvalidInputException(e)
+        self._require_valid_suggestion_and_target_types(
+            target_type, suggestion_type)
+        suggestions = suggestion_services.get_submitted_suggestions(
+            self.user_id, suggestion_type)
+        self._render_suggestions(target_type, suggestions)
 
 
 class SuggestionListHandler(base.BaseHandler):

{"golden_diff": "diff --git a/core/controllers/suggestion.py b/core/controllers/suggestion.py\n--- a/core/controllers/suggestion.py\n+++ b/core/controllers/suggestion.py\n@@ -31,26 +31,6 @@\n (suggestion_models,) = models.Registry.import_models([models.NAMES.suggestion])\n \n \n-def _require_valid_suggestion_and_target_types(target_type, suggestion_type):\n-    \"\"\"Checks whether the given target_type and suggestion_type are valid.\n-\n-    Args:\n-        target_type: str. The type of the suggestion target.\n-        suggestion_type: str. The type of the suggestion.\n-\n-    Raises:\n-        InvalidInputException: If the given target_type of suggestion_type are\n-            invalid.\n-    \"\"\"\n-    if target_type not in suggestion_models.TARGET_TYPE_CHOICES:\n-        raise utils.InvalidInputException(\n-            'Invalid target_type: %s' % target_type)\n-\n-    if suggestion_type not in suggestion_models.SUGGESTION_TYPE_CHOICES:\n-        raise utils.InvalidInputException(\n-            'Invalid suggestion_type: %s' % suggestion_type)\n-\n-\n def _get_target_id_to_exploration_opportunity_dict(suggestions):\n     \"\"\"Returns a dict of target_id to exploration opportunity summary dict.\n \n@@ -207,6 +187,26 @@\n \n     GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n \n+    def _require_valid_suggestion_and_target_types(\n+            self, target_type, suggestion_type):\n+        \"\"\"Checks whether the given target_type and suggestion_type are valid.\n+\n+        Args:\n+            target_type: str. The type of the suggestion target.\n+            suggestion_type: str. The type of the suggestion.\n+\n+        Raises:\n+            InvalidInputException: If the given target_type of suggestion_type\n+                are invalid.\n+        \"\"\"\n+        if target_type not in suggestion_models.TARGET_TYPE_CHOICES:\n+            raise self.InvalidInputException(\n+                'Invalid target_type: %s' % target_type)\n+\n+        if suggestion_type not in suggestion_models.SUGGESTION_TYPE_CHOICES:\n+            raise self.InvalidInputException(\n+                'Invalid suggestion_type: %s' % suggestion_type)\n+\n     def _render_suggestions(self, target_type, suggestions):\n         \"\"\"Renders retrieved suggestions.\n \n@@ -242,14 +242,11 @@\n     @acl_decorators.can_view_reviewable_suggestions\n     def get(self, target_type, suggestion_type):\n         \"\"\"Handles GET requests.\"\"\"\n-        try:\n-            _require_valid_suggestion_and_target_types(\n-                target_type, suggestion_type)\n-            suggestions = suggestion_services.get_reviewable_suggestions(\n-                self.user_id, suggestion_type)\n-            self._render_suggestions(target_type, suggestions)\n-        except Exception as e:\n-            raise self.InvalidInputException(e)\n+        self._require_valid_suggestion_and_target_types(\n+            target_type, suggestion_type)\n+        suggestions = suggestion_services.get_reviewable_suggestions(\n+            self.user_id, suggestion_type)\n+        self._render_suggestions(target_type, suggestions)\n \n \n class UserSubmittedSuggestionsHandler(SuggestionsProviderHandler):\n@@ -260,14 +257,11 @@\n     @acl_decorators.can_suggest_changes\n     def get(self, target_type, suggestion_type):\n         \"\"\"Handles GET requests.\"\"\"\n-        try:\n-            _require_valid_suggestion_and_target_types(\n-                target_type, suggestion_type)\n-            suggestions = suggestion_services.get_submitted_suggestions(\n-                self.user_id, suggestion_type)\n-            self._render_suggestions(target_type, suggestions)\n-        except Exception as e:\n-            raise self.InvalidInputException(e)\n+        self._require_valid_suggestion_and_target_types(\n+            target_type, suggestion_type)\n+        suggestions = suggestion_services.get_submitted_suggestions(\n+            self.user_id, suggestion_type)\n+        self._render_suggestions(target_type, suggestions)\n \n \n class SuggestionListHandler(base.BaseHandler):\n", "issue": "[Contributor Dashboard] Ensure try/catch blocks in suggestion.py print useful stack traces.\nSpecifically we want to ensure the try/catch blocks `ReviewableSuggestionsHandler` and `UserSubmittedSuggestionsHandler` print useful stack traces for debugging. Because Python omits any part of the stack trace \"below\" the top-level exception in a try/catch block, it is difficult to trace the origin of an exception during debugging. This cost a lot of time when we were debugging the contributor dashboard on a test server.\r\n\r\nSome options include removing the try/catch block or using something similar to [`traceback.format_exc()`](https://stackoverflow.com/a/3702847) to print useful stack traces.\n[Contributor Dashboard] Ensure try/catch blocks in suggestion.py print useful stack traces.\nSpecifically we want to ensure the try/catch blocks `ReviewableSuggestionsHandler` and `UserSubmittedSuggestionsHandler` print useful stack traces for debugging. Because Python omits any part of the stack trace \"below\" the top-level exception in a try/catch block, it is difficult to trace the origin of an exception during debugging. This cost a lot of time when we were debugging the contributor dashboard on a test server.\r\n\r\nSome options include removing the try/catch block or using something similar to [`traceback.format_exc()`](https://stackoverflow.com/a/3702847) to print useful stack traces.\n", "before_files": [{"content": "# coding: utf-8\n#\n# Copyright 2018 The Oppia Authors. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS-IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"Controllers for suggestions.\"\"\"\n\nfrom __future__ import absolute_import  # pylint: disable=import-only-modules\nfrom __future__ import unicode_literals  # pylint: disable=import-only-modules\n\nfrom core.controllers import acl_decorators\nfrom core.controllers import base\nfrom core.domain import opportunity_services\nfrom core.domain import skill_fetchers\nfrom core.domain import suggestion_services\nfrom core.platform import models\nimport feconf\nimport utils\n\n(suggestion_models,) = models.Registry.import_models([models.NAMES.suggestion])\n\n\ndef _require_valid_suggestion_and_target_types(target_type, suggestion_type):\n    \"\"\"Checks whether the given target_type and suggestion_type are valid.\n\n    Args:\n        target_type: str. The type of the suggestion target.\n        suggestion_type: str. The type of the suggestion.\n\n    Raises:\n        InvalidInputException: If the given target_type of suggestion_type are\n            invalid.\n    \"\"\"\n    if target_type not in suggestion_models.TARGET_TYPE_CHOICES:\n        raise utils.InvalidInputException(\n            'Invalid target_type: %s' % target_type)\n\n    if suggestion_type not in suggestion_models.SUGGESTION_TYPE_CHOICES:\n        raise utils.InvalidInputException(\n            'Invalid suggestion_type: %s' % suggestion_type)\n\n\ndef _get_target_id_to_exploration_opportunity_dict(suggestions):\n    \"\"\"Returns a dict of target_id to exploration opportunity summary dict.\n\n    Args:\n        suggestions: list(BaseSuggestion). A list of suggestions to retrieve\n            opportunity dicts.\n\n    Returns:\n        dict. Dict mapping target_id to corresponding exploration opportunity\n        summary dict.\n    \"\"\"\n    target_ids = set([s.target_id for s in suggestions])\n    opportunities = (\n        opportunity_services.get_exploration_opportunity_summaries_by_ids(\n            list(target_ids)))\n    return {opp.id: opp.to_dict() for opp in opportunities}\n\n\ndef _get_target_id_to_skill_opportunity_dict(suggestions):\n    \"\"\"Returns a dict of target_id to skill opportunity summary dict.\n\n    Args:\n        suggestions: list(BaseSuggestion). A list of suggestions to retrieve\n            opportunity dicts.\n\n    Returns:\n        dict. Dict mapping target_id to corresponding skill opportunity dict.\n    \"\"\"\n    target_ids = set([s.target_id for s in suggestions])\n    opportunities = (\n        opportunity_services.get_skill_opportunities_by_ids(list(target_ids)))\n    opportunity_skill_ids = [opp.id for opp in opportunities]\n    opportunity_id_to_skill = {\n        skill.id: skill\n        for skill in skill_fetchers.get_multi_skills(opportunity_skill_ids)\n    }\n    opportunity_id_to_opportunity = {}\n    for opp in opportunities:\n        opp_dict = opp.to_dict()\n        skill = opportunity_id_to_skill.get(opp.id)\n        if skill is not None:\n            opp_dict['skill_rubrics'] = [\n                rubric.to_dict() for rubric in skill.rubrics]\n        opportunity_id_to_opportunity[opp.id] = opp_dict\n    return opportunity_id_to_opportunity\n\n\nclass SuggestionHandler(base.BaseHandler):\n    \"\"\"\"Handles operations relating to suggestions.\"\"\"\n\n    @acl_decorators.can_suggest_changes\n    def post(self):\n        try:\n            suggestion_services.create_suggestion(\n                self.payload.get('suggestion_type'),\n                self.payload.get('target_type'), self.payload.get('target_id'),\n                self.payload.get('target_version_at_submission'),\n                self.user_id, self.payload.get('change'),\n                self.payload.get('description'))\n        except utils.ValidationError as e:\n            raise self.InvalidInputException(e)\n        self.render_json(self.values)\n\n\nclass SuggestionToExplorationActionHandler(base.BaseHandler):\n    \"\"\"Handles actions performed on suggestions to explorations.\"\"\"\n\n    @acl_decorators.get_decorator_for_accepting_suggestion(\n        acl_decorators.can_edit_exploration)\n    def put(self, target_id, suggestion_id):\n        if (\n                suggestion_id.split('.')[0] !=\n                suggestion_models.TARGET_TYPE_EXPLORATION):\n            raise self.InvalidInputException('This handler allows actions only'\n                                             ' on suggestions to explorations.')\n\n        if suggestion_id.split('.')[1] != target_id:\n            raise self.InvalidInputException('The exploration id provided does '\n                                             'not match the exploration id '\n                                             'present as part of the '\n                                             'suggestion_id')\n\n        action = self.payload.get('action')\n        suggestion = suggestion_services.get_suggestion_by_id(suggestion_id)\n\n        if suggestion.author_id == self.user_id:\n            raise self.UnauthorizedUserException('You cannot accept/reject your'\n                                                 ' own suggestion.')\n\n        if action == suggestion_models.ACTION_TYPE_ACCEPT:\n            suggestion_services.accept_suggestion(\n                suggestion, self.user_id, self.payload.get('commit_message'),\n                self.payload.get('review_message'))\n        elif action == suggestion_models.ACTION_TYPE_REJECT:\n            suggestion_services.reject_suggestion(\n                suggestion, self.user_id, self.payload.get('review_message'))\n        else:\n            raise self.InvalidInputException('Invalid action.')\n\n        self.render_json(self.values)\n\n\nclass ResubmitSuggestionHandler(base.BaseHandler):\n    \"\"\"Handler to reopen a rejected suggestion.\"\"\"\n\n    @acl_decorators.can_resubmit_suggestion\n    def put(self, suggestion_id):\n        suggestion = suggestion_services.get_suggestion_by_id(suggestion_id)\n        new_change = self.payload.get('change')\n        change_cls = type(suggestion.change)\n        change_object = change_cls(new_change)\n        suggestion.pre_update_validate(change_object)\n        suggestion.change = change_object\n        summary_message = self.payload.get('summary_message')\n        suggestion_services.resubmit_rejected_suggestion(\n            suggestion, summary_message, self.user_id)\n        self.render_json(self.values)\n\n\nclass SuggestionToSkillActionHandler(base.BaseHandler):\n    \"\"\"Handles actions performed on suggestions to skills.\"\"\"\n\n    @acl_decorators.get_decorator_for_accepting_suggestion(\n        acl_decorators.can_edit_skill)\n    def put(self, target_id, suggestion_id):\n        if suggestion_id.split('.')[0] != suggestion_models.TARGET_TYPE_SKILL:\n            raise self.InvalidInputException(\n                'This handler allows actions only on suggestions to skills.')\n\n        if suggestion_id.split('.')[1] != target_id:\n            raise self.InvalidInputException(\n                'The skill id provided does not match the skill id present as '\n                'part of the suggestion_id')\n\n        action = self.payload.get('action')\n        suggestion = suggestion_services.get_suggestion_by_id(suggestion_id)\n\n        if action == suggestion_models.ACTION_TYPE_ACCEPT:\n            # Question suggestions do not use commit messages.\n            suggestion_services.accept_suggestion(\n                suggestion, self.user_id, 'UNUSED_COMMIT_MESSAGE',\n                self.payload.get('review_message'))\n        elif action == suggestion_models.ACTION_TYPE_REJECT:\n            suggestion_services.reject_suggestion(\n                suggestion, self.user_id, self.payload.get('review_message'))\n        else:\n            raise self.InvalidInputException('Invalid action.')\n\n        self.render_json(self.values)\n\n\nclass SuggestionsProviderHandler(base.BaseHandler):\n    \"\"\"Provides suggestions for a user and given suggestion type.\"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n\n    def _render_suggestions(self, target_type, suggestions):\n        \"\"\"Renders retrieved suggestions.\n\n        Args:\n            target_type: str. The suggestion type.\n            suggestions: list(BaseSuggestion). A list of suggestions to render.\n        \"\"\"\n        if target_type == suggestion_models.TARGET_TYPE_EXPLORATION:\n            target_id_to_opportunity_dict = (\n                _get_target_id_to_exploration_opportunity_dict(suggestions))\n            self.render_json({\n                'suggestions': [s.to_dict() for s in suggestions],\n                'target_id_to_opportunity_dict':\n                    target_id_to_opportunity_dict\n            })\n        elif target_type == suggestion_models.TARGET_TYPE_SKILL:\n            target_id_to_opportunity_dict = (\n                _get_target_id_to_skill_opportunity_dict(suggestions))\n            self.render_json({\n                'suggestions': [s.to_dict() for s in suggestions],\n                'target_id_to_opportunity_dict':\n                    target_id_to_opportunity_dict\n            })\n        else:\n            self.render_json({})\n\n\nclass ReviewableSuggestionsHandler(SuggestionsProviderHandler):\n    \"\"\"Provides all suggestions which can be reviewed by the user for a given\n    suggestion type.\n    \"\"\"\n\n    @acl_decorators.can_view_reviewable_suggestions\n    def get(self, target_type, suggestion_type):\n        \"\"\"Handles GET requests.\"\"\"\n        try:\n            _require_valid_suggestion_and_target_types(\n                target_type, suggestion_type)\n            suggestions = suggestion_services.get_reviewable_suggestions(\n                self.user_id, suggestion_type)\n            self._render_suggestions(target_type, suggestions)\n        except Exception as e:\n            raise self.InvalidInputException(e)\n\n\nclass UserSubmittedSuggestionsHandler(SuggestionsProviderHandler):\n    \"\"\"Provides all suggestions which are submitted by the user for a given\n    suggestion type.\n    \"\"\"\n\n    @acl_decorators.can_suggest_changes\n    def get(self, target_type, suggestion_type):\n        \"\"\"Handles GET requests.\"\"\"\n        try:\n            _require_valid_suggestion_and_target_types(\n                target_type, suggestion_type)\n            suggestions = suggestion_services.get_submitted_suggestions(\n                self.user_id, suggestion_type)\n            self._render_suggestions(target_type, suggestions)\n        except Exception as e:\n            raise self.InvalidInputException(e)\n\n\nclass SuggestionListHandler(base.BaseHandler):\n    \"\"\"Handles list operations on suggestions.\"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n\n    @acl_decorators.open_access\n    def get(self):\n        # The query_fields_and_values variable is a list of tuples. The first\n        # element in each tuple is the field being queried and the second\n        # element is the value of the field being queried.\n        # request.GET.items() parses the params from the url into the above\n        # format. So in the url, the query should be passed as:\n        # ?field1=value1&field2=value2...fieldN=valueN.\n        query_fields_and_values = list(self.request.GET.items())\n\n        for query in query_fields_and_values:\n            if query[0] not in suggestion_models.ALLOWED_QUERY_FIELDS:\n                raise self.InvalidInputException(\n                    'Not allowed to query on field %s' % query[0])\n\n        suggestions = suggestion_services.query_suggestions(\n            query_fields_and_values)\n\n        self.values.update({'suggestions': [s.to_dict() for s in suggestions]})\n        self.render_json(self.values)\n", "path": "core/controllers/suggestion.py"}], "after_files": [{"content": "# coding: utf-8\n#\n# Copyright 2018 The Oppia Authors. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS-IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"Controllers for suggestions.\"\"\"\n\nfrom __future__ import absolute_import  # pylint: disable=import-only-modules\nfrom __future__ import unicode_literals  # pylint: disable=import-only-modules\n\nfrom core.controllers import acl_decorators\nfrom core.controllers import base\nfrom core.domain import opportunity_services\nfrom core.domain import skill_fetchers\nfrom core.domain import suggestion_services\nfrom core.platform import models\nimport feconf\nimport utils\n\n(suggestion_models,) = models.Registry.import_models([models.NAMES.suggestion])\n\n\ndef _get_target_id_to_exploration_opportunity_dict(suggestions):\n    \"\"\"Returns a dict of target_id to exploration opportunity summary dict.\n\n    Args:\n        suggestions: list(BaseSuggestion). A list of suggestions to retrieve\n            opportunity dicts.\n\n    Returns:\n        dict. Dict mapping target_id to corresponding exploration opportunity\n        summary dict.\n    \"\"\"\n    target_ids = set([s.target_id for s in suggestions])\n    opportunities = (\n        opportunity_services.get_exploration_opportunity_summaries_by_ids(\n            list(target_ids)))\n    return {opp.id: opp.to_dict() for opp in opportunities}\n\n\ndef _get_target_id_to_skill_opportunity_dict(suggestions):\n    \"\"\"Returns a dict of target_id to skill opportunity summary dict.\n\n    Args:\n        suggestions: list(BaseSuggestion). A list of suggestions to retrieve\n            opportunity dicts.\n\n    Returns:\n        dict. Dict mapping target_id to corresponding skill opportunity dict.\n    \"\"\"\n    target_ids = set([s.target_id for s in suggestions])\n    opportunities = (\n        opportunity_services.get_skill_opportunities_by_ids(list(target_ids)))\n    opportunity_skill_ids = [opp.id for opp in opportunities]\n    opportunity_id_to_skill = {\n        skill.id: skill\n        for skill in skill_fetchers.get_multi_skills(opportunity_skill_ids)\n    }\n    opportunity_id_to_opportunity = {}\n    for opp in opportunities:\n        opp_dict = opp.to_dict()\n        skill = opportunity_id_to_skill.get(opp.id)\n        if skill is not None:\n            opp_dict['skill_rubrics'] = [\n                rubric.to_dict() for rubric in skill.rubrics]\n        opportunity_id_to_opportunity[opp.id] = opp_dict\n    return opportunity_id_to_opportunity\n\n\nclass SuggestionHandler(base.BaseHandler):\n    \"\"\"\"Handles operations relating to suggestions.\"\"\"\n\n    @acl_decorators.can_suggest_changes\n    def post(self):\n        try:\n            suggestion_services.create_suggestion(\n                self.payload.get('suggestion_type'),\n                self.payload.get('target_type'), self.payload.get('target_id'),\n                self.payload.get('target_version_at_submission'),\n                self.user_id, self.payload.get('change'),\n                self.payload.get('description'))\n        except utils.ValidationError as e:\n            raise self.InvalidInputException(e)\n        self.render_json(self.values)\n\n\nclass SuggestionToExplorationActionHandler(base.BaseHandler):\n    \"\"\"Handles actions performed on suggestions to explorations.\"\"\"\n\n    @acl_decorators.get_decorator_for_accepting_suggestion(\n        acl_decorators.can_edit_exploration)\n    def put(self, target_id, suggestion_id):\n        if (\n                suggestion_id.split('.')[0] !=\n                suggestion_models.TARGET_TYPE_EXPLORATION):\n            raise self.InvalidInputException('This handler allows actions only'\n                                             ' on suggestions to explorations.')\n\n        if suggestion_id.split('.')[1] != target_id:\n            raise self.InvalidInputException('The exploration id provided does '\n                                             'not match the exploration id '\n                                             'present as part of the '\n                                             'suggestion_id')\n\n        action = self.payload.get('action')\n        suggestion = suggestion_services.get_suggestion_by_id(suggestion_id)\n\n        if suggestion.author_id == self.user_id:\n            raise self.UnauthorizedUserException('You cannot accept/reject your'\n                                                 ' own suggestion.')\n\n        if action == suggestion_models.ACTION_TYPE_ACCEPT:\n            suggestion_services.accept_suggestion(\n                suggestion, self.user_id, self.payload.get('commit_message'),\n                self.payload.get('review_message'))\n        elif action == suggestion_models.ACTION_TYPE_REJECT:\n            suggestion_services.reject_suggestion(\n                suggestion, self.user_id, self.payload.get('review_message'))\n        else:\n            raise self.InvalidInputException('Invalid action.')\n\n        self.render_json(self.values)\n\n\nclass ResubmitSuggestionHandler(base.BaseHandler):\n    \"\"\"Handler to reopen a rejected suggestion.\"\"\"\n\n    @acl_decorators.can_resubmit_suggestion\n    def put(self, suggestion_id):\n        suggestion = suggestion_services.get_suggestion_by_id(suggestion_id)\n        new_change = self.payload.get('change')\n        change_cls = type(suggestion.change)\n        change_object = change_cls(new_change)\n        suggestion.pre_update_validate(change_object)\n        suggestion.change = change_object\n        summary_message = self.payload.get('summary_message')\n        suggestion_services.resubmit_rejected_suggestion(\n            suggestion, summary_message, self.user_id)\n        self.render_json(self.values)\n\n\nclass SuggestionToSkillActionHandler(base.BaseHandler):\n    \"\"\"Handles actions performed on suggestions to skills.\"\"\"\n\n    @acl_decorators.get_decorator_for_accepting_suggestion(\n        acl_decorators.can_edit_skill)\n    def put(self, target_id, suggestion_id):\n        if suggestion_id.split('.')[0] != suggestion_models.TARGET_TYPE_SKILL:\n            raise self.InvalidInputException(\n                'This handler allows actions only on suggestions to skills.')\n\n        if suggestion_id.split('.')[1] != target_id:\n            raise self.InvalidInputException(\n                'The skill id provided does not match the skill id present as '\n                'part of the suggestion_id')\n\n        action = self.payload.get('action')\n        suggestion = suggestion_services.get_suggestion_by_id(suggestion_id)\n\n        if action == suggestion_models.ACTION_TYPE_ACCEPT:\n            # Question suggestions do not use commit messages.\n            suggestion_services.accept_suggestion(\n                suggestion, self.user_id, 'UNUSED_COMMIT_MESSAGE',\n                self.payload.get('review_message'))\n        elif action == suggestion_models.ACTION_TYPE_REJECT:\n            suggestion_services.reject_suggestion(\n                suggestion, self.user_id, self.payload.get('review_message'))\n        else:\n            raise self.InvalidInputException('Invalid action.')\n\n        self.render_json(self.values)\n\n\nclass SuggestionsProviderHandler(base.BaseHandler):\n    \"\"\"Provides suggestions for a user and given suggestion type.\"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n\n    def _require_valid_suggestion_and_target_types(\n            self, target_type, suggestion_type):\n        \"\"\"Checks whether the given target_type and suggestion_type are valid.\n\n        Args:\n            target_type: str. The type of the suggestion target.\n            suggestion_type: str. The type of the suggestion.\n\n        Raises:\n            InvalidInputException: If the given target_type of suggestion_type\n                are invalid.\n        \"\"\"\n        if target_type not in suggestion_models.TARGET_TYPE_CHOICES:\n            raise self.InvalidInputException(\n                'Invalid target_type: %s' % target_type)\n\n        if suggestion_type not in suggestion_models.SUGGESTION_TYPE_CHOICES:\n            raise self.InvalidInputException(\n                'Invalid suggestion_type: %s' % suggestion_type)\n\n    def _render_suggestions(self, target_type, suggestions):\n        \"\"\"Renders retrieved suggestions.\n\n        Args:\n            target_type: str. The suggestion type.\n            suggestions: list(BaseSuggestion). A list of suggestions to render.\n        \"\"\"\n        if target_type == suggestion_models.TARGET_TYPE_EXPLORATION:\n            target_id_to_opportunity_dict = (\n                _get_target_id_to_exploration_opportunity_dict(suggestions))\n            self.render_json({\n                'suggestions': [s.to_dict() for s in suggestions],\n                'target_id_to_opportunity_dict':\n                    target_id_to_opportunity_dict\n            })\n        elif target_type == suggestion_models.TARGET_TYPE_SKILL:\n            target_id_to_opportunity_dict = (\n                _get_target_id_to_skill_opportunity_dict(suggestions))\n            self.render_json({\n                'suggestions': [s.to_dict() for s in suggestions],\n                'target_id_to_opportunity_dict':\n                    target_id_to_opportunity_dict\n            })\n        else:\n            self.render_json({})\n\n\nclass ReviewableSuggestionsHandler(SuggestionsProviderHandler):\n    \"\"\"Provides all suggestions which can be reviewed by the user for a given\n    suggestion type.\n    \"\"\"\n\n    @acl_decorators.can_view_reviewable_suggestions\n    def get(self, target_type, suggestion_type):\n        \"\"\"Handles GET requests.\"\"\"\n        self._require_valid_suggestion_and_target_types(\n            target_type, suggestion_type)\n        suggestions = suggestion_services.get_reviewable_suggestions(\n            self.user_id, suggestion_type)\n        self._render_suggestions(target_type, suggestions)\n\n\nclass UserSubmittedSuggestionsHandler(SuggestionsProviderHandler):\n    \"\"\"Provides all suggestions which are submitted by the user for a given\n    suggestion type.\n    \"\"\"\n\n    @acl_decorators.can_suggest_changes\n    def get(self, target_type, suggestion_type):\n        \"\"\"Handles GET requests.\"\"\"\n        self._require_valid_suggestion_and_target_types(\n            target_type, suggestion_type)\n        suggestions = suggestion_services.get_submitted_suggestions(\n            self.user_id, suggestion_type)\n        self._render_suggestions(target_type, suggestions)\n\n\nclass SuggestionListHandler(base.BaseHandler):\n    \"\"\"Handles list operations on suggestions.\"\"\"\n\n    GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON\n\n    @acl_decorators.open_access\n    def get(self):\n        # The query_fields_and_values variable is a list of tuples. The first\n        # element in each tuple is the field being queried and the second\n        # element is the value of the field being queried.\n        # request.GET.items() parses the params from the url into the above\n        # format. So in the url, the query should be passed as:\n        # ?field1=value1&field2=value2...fieldN=valueN.\n        query_fields_and_values = list(self.request.GET.items())\n\n        for query in query_fields_and_values:\n            if query[0] not in suggestion_models.ALLOWED_QUERY_FIELDS:\n                raise self.InvalidInputException(\n                    'Not allowed to query on field %s' % query[0])\n\n        suggestions = suggestion_services.query_suggestions(\n            query_fields_and_values)\n\n        self.values.update({'suggestions': [s.to_dict() for s in suggestions]})\n        self.render_json(self.values)\n", "path": "core/controllers/suggestion.py"}]}