We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Update the "collections" section of the creator dashboard to use the newer summary tiles.
In the creator dashboard, the "collections" tab shows long horizontal cards. These are out of sync with the rest of the site, and should instead be replaced with the smaller collection summary tiles that are used in the library page.


--- 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/dashboard.py`
Content:
```
1 # Copyright 2014 The Oppia Authors. All Rights Reserved.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS-IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 """Controllers for the creator dashboard, notifications, and creating new
16 activities.
17 """
18 
19 from core.controllers import base
20 from core.domain import collection_domain
21 from core.domain import collection_services
22 from core.domain import config_domain
23 from core.domain import exp_domain
24 from core.domain import exp_services
25 from core.domain import feedback_services
26 from core.domain import stats_services
27 from core.domain import subscription_services
28 from core.domain import summary_services
29 from core.domain import user_jobs_continuous
30 from core.domain import user_services
31 import feconf
32 import utils
33 
34 EXPLORATION_ID_KEY = 'explorationId'
35 COLLECTION_ID_KEY = 'collectionId'
36 
37 DEFAULT_TWITTER_SHARE_MESSAGE_DASHBOARD = config_domain.ConfigProperty(
38     'default_twitter_share_message_dashboard', {
39         'type': 'unicode',
40     },
41     'Default text for the Twitter share message for the dashboard',
42     default_value=(
43         'Check out this interactive lesson I created on Oppia - a free '
44         'platform for teaching and learning!'))
45 
46 
47 class NotificationsDashboardPage(base.BaseHandler):
48     """Page with notifications for the user."""
49 
50     @base.require_user
51     def get(self):
52         if self.username in config_domain.BANNED_USERNAMES.value:
53             raise self.UnauthorizedUserException(
54                 'You do not have the credentials to access this page.')
55         elif user_services.has_fully_registered(self.user_id):
56             self.values.update({
57                 'meta_description': feconf.DASHBOARD_PAGE_DESCRIPTION,
58                 'nav_mode': feconf.NAV_MODE_DASHBOARD,
59             })
60             self.render_template(
61                 'dashboard/notifications_dashboard.html',
62                 redirect_url_on_logout='/')
63         else:
64             self.redirect(utils.set_url_query_parameter(
65                 feconf.SIGNUP_URL, 'return_url', '/notifications_dashboard'))
66 
67 
68 class NotificationsDashboardHandler(base.BaseHandler):
69     """Provides data for the user notifications dashboard."""
70 
71     def get(self):
72         """Handles GET requests."""
73         if self.user_id is None:
74             raise self.PageNotFoundException
75 
76         job_queued_msec, recent_notifications = (
77             user_jobs_continuous.DashboardRecentUpdatesAggregator.get_recent_notifications(  # pylint: disable=line-too-long
78                 self.user_id))
79 
80         last_seen_msec = (
81             subscription_services.get_last_seen_notifications_msec(
82                 self.user_id))
83 
84         # Replace author_ids with their usernames.
85         author_ids = [
86             notification['author_id'] for notification in recent_notifications
87             if notification['author_id']]
88         author_usernames = user_services.get_usernames(author_ids)
89 
90         author_id_to_username = {
91             None: '',
92         }
93         for ind, author_id in enumerate(author_ids):
94             author_id_to_username[author_id] = author_usernames[ind]
95         for notification in recent_notifications:
96             notification['author_username'] = (
97                 author_id_to_username[notification['author_id']])
98             del notification['author_id']
99 
100         subscription_services.record_user_has_seen_notifications(
101             self.user_id, job_queued_msec if job_queued_msec else 0.0)
102 
103         self.values.update({
104             # This may be None if no job has ever run for this user.
105             'job_queued_msec': job_queued_msec,
106             # This may be None if this is the first time the user has seen
107             # the dashboard.
108             'last_seen_msec': last_seen_msec,
109             'recent_notifications': recent_notifications,
110         })
111         self.render_json(self.values)
112 
113 
114 class DashboardPage(base.BaseHandler):
115     """Page showing the user's creator dashboard."""
116 
117     @base.require_user
118     def get(self):
119         if self.username in config_domain.BANNED_USERNAMES.value:
120             raise self.UnauthorizedUserException(
121                 'You do not have the credentials to access this page.')
122         elif user_services.has_fully_registered(self.user_id):
123             self.values.update({
124                 'nav_mode': feconf.NAV_MODE_DASHBOARD,
125                 'allow_yaml_file_upload': feconf.ALLOW_YAML_FILE_UPLOAD,
126                 'DEFAULT_TWITTER_SHARE_MESSAGE_DASHBOARD': (
127                     DEFAULT_TWITTER_SHARE_MESSAGE_DASHBOARD.value)
128             })
129             self.render_template(
130                 'dashboard/dashboard.html', redirect_url_on_logout='/')
131         else:
132             self.redirect(utils.set_url_query_parameter(
133                 feconf.SIGNUP_URL, 'return_url', feconf.DASHBOARD_URL))
134 
135 
136 class DashboardHandler(base.BaseHandler):
137     """Provides data for the user's creator dashboard page."""
138 
139     def get(self):
140         """Handles GET requests."""
141         if self.user_id is None:
142             raise self.PageNotFoundException
143 
144         def _get_intro_card_color(category):
145             return (
146                 feconf.CATEGORIES_TO_COLORS[category] if
147                 category in feconf.CATEGORIES_TO_COLORS else
148                 feconf.DEFAULT_COLOR)
149 
150         def _round_average_ratings(rating):
151             return round(rating, feconf.AVERAGE_RATINGS_DASHBOARD_PRECISION)
152 
153         exploration_ids_subscribed_to = (
154             subscription_services.get_exploration_ids_subscribed_to(
155                 self.user_id))
156 
157         subscribed_exploration_summaries = filter(None, (
158             exp_services.get_exploration_summaries_matching_ids(
159                 exploration_ids_subscribed_to)))
160         subscribed_collection_summaries = filter(None, (
161             collection_services.get_collection_summaries_matching_ids(
162                 subscription_services.get_collection_ids_subscribed_to(
163                     self.user_id))))
164 
165         exp_summary_list = summary_services.get_displayable_exp_summary_dicts(
166             subscribed_exploration_summaries)
167         collection_summary_list = []
168 
169         feedback_thread_analytics = (
170             feedback_services.get_thread_analytics_multi(
171                 exploration_ids_subscribed_to))
172 
173         unresolved_answers_dict = (
174             stats_services.get_exps_unresolved_answers_count_for_default_rule(
175                 exploration_ids_subscribed_to))
176 
177         for ind, exploration in enumerate(exp_summary_list):
178             exploration.update(feedback_thread_analytics[ind].to_dict())
179             exploration.update({
180                 'num_unresolved_answers': (
181                     unresolved_answers_dict[exploration['id']]
182                     if exploration['id'] in unresolved_answers_dict else 0
183                 )
184             })
185 
186         exp_summary_list = sorted(
187             exp_summary_list,
188             key=lambda x: (x['num_open_threads'], x['last_updated_msec']),
189             reverse=True)
190 
191         if (self.username in
192                 config_domain.WHITELISTED_COLLECTION_EDITOR_USERNAMES.value):
193             for collection_summary in subscribed_collection_summaries:
194                 # TODO(sll): Reuse _get_displayable_collection_summary_dicts()
195                 # in summary_services, instead of replicating it like this.
196                 collection_summary_list.append({
197                     'id': collection_summary.id,
198                     'title': collection_summary.title,
199                     'category': collection_summary.category,
200                     'objective': collection_summary.objective,
201                     'language_code': collection_summary.language_code,
202                     'last_updated': utils.get_time_in_millisecs(
203                         collection_summary.collection_model_last_updated),
204                     'created_on': utils.get_time_in_millisecs(
205                         collection_summary.collection_model_created_on),
206                     'status': collection_summary.status,
207                     'community_owned': collection_summary.community_owned,
208                     'thumbnail_icon_url': (
209                         utils.get_thumbnail_icon_url_for_category(
210                             collection_summary.category)),
211                     'thumbnail_bg_color': utils.get_hex_color_for_category(
212                         collection_summary.category),
213                 })
214 
215         dashboard_stats = (
216             user_jobs_continuous.UserStatsAggregator.get_dashboard_stats(
217                 self.user_id))
218         dashboard_stats.update({
219             'total_open_feedback': feedback_services.get_total_open_threads(
220                 feedback_thread_analytics)
221         })
222         if dashboard_stats and dashboard_stats.get('average_ratings'):
223             dashboard_stats['average_ratings'] = (
224                 _round_average_ratings(dashboard_stats['average_ratings']))
225 
226         last_week_stats = (
227             user_services.get_last_week_dashboard_stats(self.user_id))
228         if last_week_stats and last_week_stats.get('average_ratings'):
229             last_week_stats['average_ratings'] = (
230                 _round_average_ratings(last_week_stats['average_ratings']))
231 
232         self.values.update({
233             'explorations_list': exp_summary_list,
234             'collections_list': collection_summary_list,
235             'dashboard_stats': dashboard_stats,
236             'last_week_stats': last_week_stats
237         })
238         self.render_json(self.values)
239 
240 
241 class NotificationsHandler(base.BaseHandler):
242     """Provides data about unseen notifications."""
243 
244     def get(self):
245         """Handles GET requests."""
246         num_unseen_notifications = 0
247         if self.user_id and self.username:
248             last_seen_msec = (
249                 subscription_services.get_last_seen_notifications_msec(
250                     self.user_id))
251             _, recent_notifications = (
252                 user_jobs_continuous.DashboardRecentUpdatesAggregator.get_recent_notifications( # pylint: disable=line-too-long
253                     self.user_id))
254             for notification in recent_notifications:
255                 if (notification['last_updated_ms'] > last_seen_msec and
256                         notification['author_id'] != self.user_id):
257                     num_unseen_notifications += 1
258 
259         self.render_json({
260             'num_unseen_notifications': num_unseen_notifications,
261         })
262 
263 
264 class NewExploration(base.BaseHandler):
265     """Creates a new exploration."""
266 
267     @base.require_fully_signed_up
268     def post(self):
269         """Handles POST requests."""
270         title = self.payload.get('title', feconf.DEFAULT_EXPLORATION_TITLE)
271 
272         new_exploration_id = exp_services.get_new_exploration_id()
273         exploration = exp_domain.Exploration.create_default_exploration(
274             new_exploration_id, title=title)
275         exp_services.save_new_exploration(self.user_id, exploration)
276 
277         self.render_json({
278             EXPLORATION_ID_KEY: new_exploration_id
279         })
280 
281 
282 class NewCollection(base.BaseHandler):
283     """Creates a new collection."""
284 
285     @base.require_fully_signed_up
286     def post(self):
287         """Handles POST requests."""
288         new_collection_id = collection_services.get_new_collection_id()
289         collection = collection_domain.Collection.create_default_collection(
290             new_collection_id)
291         collection_services.save_new_collection(self.user_id, collection)
292 
293         self.render_json({
294             COLLECTION_ID_KEY: new_collection_id
295         })
296 
297 
298 class UploadExploration(base.BaseHandler):
299     """Uploads a new exploration."""
300 
301     @base.require_fully_signed_up
302     def post(self):
303         """Handles POST requests."""
304         yaml_content = self.request.get('yaml_file')
305 
306         new_exploration_id = exp_services.get_new_exploration_id()
307         if feconf.ALLOW_YAML_FILE_UPLOAD:
308             exp_services.save_new_exploration_from_yaml_and_assets(
309                 self.user_id, yaml_content, new_exploration_id, [])
310             self.render_json({
311                 EXPLORATION_ID_KEY: new_exploration_id
312             })
313         else:
314             raise self.InvalidInputException(
315                 'This server does not allow file uploads.')
316 
317 
318 class DashboardRedirectPage(base.BaseHandler):
319     """An page that redirects to the main Dashboard page."""
320 
321     def get(self):
322         """Handles GET requests."""
323         self.redirect(feconf.DASHBOARD_URL)
324
```
--- 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/dashboard.py b/core/controllers/dashboard.py
--- a/core/controllers/dashboard.py
+++ b/core/controllers/dashboard.py
@@ -204,6 +204,7 @@
                     'created_on': utils.get_time_in_millisecs(
                         collection_summary.collection_model_created_on),
                     'status': collection_summary.status,
+                    'node_count': collection_summary.node_count,
                     'community_owned': collection_summary.community_owned,
                     'thumbnail_icon_url': (
                         utils.get_thumbnail_icon_url_for_category(

{"golden_diff": "diff --git a/core/controllers/dashboard.py b/core/controllers/dashboard.py\n--- a/core/controllers/dashboard.py\n+++ b/core/controllers/dashboard.py\n@@ -204,6 +204,7 @@\n                     'created_on': utils.get_time_in_millisecs(\n                         collection_summary.collection_model_created_on),\n                     'status': collection_summary.status,\n+                    'node_count': collection_summary.node_count,\n                     'community_owned': collection_summary.community_owned,\n                     'thumbnail_icon_url': (\n                         utils.get_thumbnail_icon_url_for_category(\n", "issue": "Update the \"collections\" section of the creator dashboard to use the newer summary tiles.\nIn the creator dashboard, the \"collections\" tab shows long horizontal cards. These are out of sync with the rest of the site, and should instead be replaced with the smaller collection summary tiles that are used in the library page.\n\n", "before_files": [{"content": "# Copyright 2014 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 the creator dashboard, notifications, and creating new\nactivities.\n\"\"\"\n\nfrom core.controllers import base\nfrom core.domain import collection_domain\nfrom core.domain import collection_services\nfrom core.domain import config_domain\nfrom core.domain import exp_domain\nfrom core.domain import exp_services\nfrom core.domain import feedback_services\nfrom core.domain import stats_services\nfrom core.domain import subscription_services\nfrom core.domain import summary_services\nfrom core.domain import user_jobs_continuous\nfrom core.domain import user_services\nimport feconf\nimport utils\n\nEXPLORATION_ID_KEY = 'explorationId'\nCOLLECTION_ID_KEY = 'collectionId'\n\nDEFAULT_TWITTER_SHARE_MESSAGE_DASHBOARD = config_domain.ConfigProperty(\n    'default_twitter_share_message_dashboard', {\n        'type': 'unicode',\n    },\n    'Default text for the Twitter share message for the dashboard',\n    default_value=(\n        'Check out this interactive lesson I created on Oppia - a free '\n        'platform for teaching and learning!'))\n\n\nclass NotificationsDashboardPage(base.BaseHandler):\n    \"\"\"Page with notifications for the user.\"\"\"\n\n    @base.require_user\n    def get(self):\n        if self.username in config_domain.BANNED_USERNAMES.value:\n            raise self.UnauthorizedUserException(\n                'You do not have the credentials to access this page.')\n        elif user_services.has_fully_registered(self.user_id):\n            self.values.update({\n                'meta_description': feconf.DASHBOARD_PAGE_DESCRIPTION,\n                'nav_mode': feconf.NAV_MODE_DASHBOARD,\n            })\n            self.render_template(\n                'dashboard/notifications_dashboard.html',\n                redirect_url_on_logout='/')\n        else:\n            self.redirect(utils.set_url_query_parameter(\n                feconf.SIGNUP_URL, 'return_url', '/notifications_dashboard'))\n\n\nclass NotificationsDashboardHandler(base.BaseHandler):\n    \"\"\"Provides data for the user notifications dashboard.\"\"\"\n\n    def get(self):\n        \"\"\"Handles GET requests.\"\"\"\n        if self.user_id is None:\n            raise self.PageNotFoundException\n\n        job_queued_msec, recent_notifications = (\n            user_jobs_continuous.DashboardRecentUpdatesAggregator.get_recent_notifications(  # pylint: disable=line-too-long\n                self.user_id))\n\n        last_seen_msec = (\n            subscription_services.get_last_seen_notifications_msec(\n                self.user_id))\n\n        # Replace author_ids with their usernames.\n        author_ids = [\n            notification['author_id'] for notification in recent_notifications\n            if notification['author_id']]\n        author_usernames = user_services.get_usernames(author_ids)\n\n        author_id_to_username = {\n            None: '',\n        }\n        for ind, author_id in enumerate(author_ids):\n            author_id_to_username[author_id] = author_usernames[ind]\n        for notification in recent_notifications:\n            notification['author_username'] = (\n                author_id_to_username[notification['author_id']])\n            del notification['author_id']\n\n        subscription_services.record_user_has_seen_notifications(\n            self.user_id, job_queued_msec if job_queued_msec else 0.0)\n\n        self.values.update({\n            # This may be None if no job has ever run for this user.\n            'job_queued_msec': job_queued_msec,\n            # This may be None if this is the first time the user has seen\n            # the dashboard.\n            'last_seen_msec': last_seen_msec,\n            'recent_notifications': recent_notifications,\n        })\n        self.render_json(self.values)\n\n\nclass DashboardPage(base.BaseHandler):\n    \"\"\"Page showing the user's creator dashboard.\"\"\"\n\n    @base.require_user\n    def get(self):\n        if self.username in config_domain.BANNED_USERNAMES.value:\n            raise self.UnauthorizedUserException(\n                'You do not have the credentials to access this page.')\n        elif user_services.has_fully_registered(self.user_id):\n            self.values.update({\n                'nav_mode': feconf.NAV_MODE_DASHBOARD,\n                'allow_yaml_file_upload': feconf.ALLOW_YAML_FILE_UPLOAD,\n                'DEFAULT_TWITTER_SHARE_MESSAGE_DASHBOARD': (\n                    DEFAULT_TWITTER_SHARE_MESSAGE_DASHBOARD.value)\n            })\n            self.render_template(\n                'dashboard/dashboard.html', redirect_url_on_logout='/')\n        else:\n            self.redirect(utils.set_url_query_parameter(\n                feconf.SIGNUP_URL, 'return_url', feconf.DASHBOARD_URL))\n\n\nclass DashboardHandler(base.BaseHandler):\n    \"\"\"Provides data for the user's creator dashboard page.\"\"\"\n\n    def get(self):\n        \"\"\"Handles GET requests.\"\"\"\n        if self.user_id is None:\n            raise self.PageNotFoundException\n\n        def _get_intro_card_color(category):\n            return (\n                feconf.CATEGORIES_TO_COLORS[category] if\n                category in feconf.CATEGORIES_TO_COLORS else\n                feconf.DEFAULT_COLOR)\n\n        def _round_average_ratings(rating):\n            return round(rating, feconf.AVERAGE_RATINGS_DASHBOARD_PRECISION)\n\n        exploration_ids_subscribed_to = (\n            subscription_services.get_exploration_ids_subscribed_to(\n                self.user_id))\n\n        subscribed_exploration_summaries = filter(None, (\n            exp_services.get_exploration_summaries_matching_ids(\n                exploration_ids_subscribed_to)))\n        subscribed_collection_summaries = filter(None, (\n            collection_services.get_collection_summaries_matching_ids(\n                subscription_services.get_collection_ids_subscribed_to(\n                    self.user_id))))\n\n        exp_summary_list = summary_services.get_displayable_exp_summary_dicts(\n            subscribed_exploration_summaries)\n        collection_summary_list = []\n\n        feedback_thread_analytics = (\n            feedback_services.get_thread_analytics_multi(\n                exploration_ids_subscribed_to))\n\n        unresolved_answers_dict = (\n            stats_services.get_exps_unresolved_answers_count_for_default_rule(\n                exploration_ids_subscribed_to))\n\n        for ind, exploration in enumerate(exp_summary_list):\n            exploration.update(feedback_thread_analytics[ind].to_dict())\n            exploration.update({\n                'num_unresolved_answers': (\n                    unresolved_answers_dict[exploration['id']]\n                    if exploration['id'] in unresolved_answers_dict else 0\n                )\n            })\n\n        exp_summary_list = sorted(\n            exp_summary_list,\n            key=lambda x: (x['num_open_threads'], x['last_updated_msec']),\n            reverse=True)\n\n        if (self.username in\n                config_domain.WHITELISTED_COLLECTION_EDITOR_USERNAMES.value):\n            for collection_summary in subscribed_collection_summaries:\n                # TODO(sll): Reuse _get_displayable_collection_summary_dicts()\n                # in summary_services, instead of replicating it like this.\n                collection_summary_list.append({\n                    'id': collection_summary.id,\n                    'title': collection_summary.title,\n                    'category': collection_summary.category,\n                    'objective': collection_summary.objective,\n                    'language_code': collection_summary.language_code,\n                    'last_updated': utils.get_time_in_millisecs(\n                        collection_summary.collection_model_last_updated),\n                    'created_on': utils.get_time_in_millisecs(\n                        collection_summary.collection_model_created_on),\n                    'status': collection_summary.status,\n                    'community_owned': collection_summary.community_owned,\n                    'thumbnail_icon_url': (\n                        utils.get_thumbnail_icon_url_for_category(\n                            collection_summary.category)),\n                    'thumbnail_bg_color': utils.get_hex_color_for_category(\n                        collection_summary.category),\n                })\n\n        dashboard_stats = (\n            user_jobs_continuous.UserStatsAggregator.get_dashboard_stats(\n                self.user_id))\n        dashboard_stats.update({\n            'total_open_feedback': feedback_services.get_total_open_threads(\n                feedback_thread_analytics)\n        })\n        if dashboard_stats and dashboard_stats.get('average_ratings'):\n            dashboard_stats['average_ratings'] = (\n                _round_average_ratings(dashboard_stats['average_ratings']))\n\n        last_week_stats = (\n            user_services.get_last_week_dashboard_stats(self.user_id))\n        if last_week_stats and last_week_stats.get('average_ratings'):\n            last_week_stats['average_ratings'] = (\n                _round_average_ratings(last_week_stats['average_ratings']))\n\n        self.values.update({\n            'explorations_list': exp_summary_list,\n            'collections_list': collection_summary_list,\n            'dashboard_stats': dashboard_stats,\n            'last_week_stats': last_week_stats\n        })\n        self.render_json(self.values)\n\n\nclass NotificationsHandler(base.BaseHandler):\n    \"\"\"Provides data about unseen notifications.\"\"\"\n\n    def get(self):\n        \"\"\"Handles GET requests.\"\"\"\n        num_unseen_notifications = 0\n        if self.user_id and self.username:\n            last_seen_msec = (\n                subscription_services.get_last_seen_notifications_msec(\n                    self.user_id))\n            _, recent_notifications = (\n                user_jobs_continuous.DashboardRecentUpdatesAggregator.get_recent_notifications( # pylint: disable=line-too-long\n                    self.user_id))\n            for notification in recent_notifications:\n                if (notification['last_updated_ms'] > last_seen_msec and\n                        notification['author_id'] != self.user_id):\n                    num_unseen_notifications += 1\n\n        self.render_json({\n            'num_unseen_notifications': num_unseen_notifications,\n        })\n\n\nclass NewExploration(base.BaseHandler):\n    \"\"\"Creates a new exploration.\"\"\"\n\n    @base.require_fully_signed_up\n    def post(self):\n        \"\"\"Handles POST requests.\"\"\"\n        title = self.payload.get('title', feconf.DEFAULT_EXPLORATION_TITLE)\n\n        new_exploration_id = exp_services.get_new_exploration_id()\n        exploration = exp_domain.Exploration.create_default_exploration(\n            new_exploration_id, title=title)\n        exp_services.save_new_exploration(self.user_id, exploration)\n\n        self.render_json({\n            EXPLORATION_ID_KEY: new_exploration_id\n        })\n\n\nclass NewCollection(base.BaseHandler):\n    \"\"\"Creates a new collection.\"\"\"\n\n    @base.require_fully_signed_up\n    def post(self):\n        \"\"\"Handles POST requests.\"\"\"\n        new_collection_id = collection_services.get_new_collection_id()\n        collection = collection_domain.Collection.create_default_collection(\n            new_collection_id)\n        collection_services.save_new_collection(self.user_id, collection)\n\n        self.render_json({\n            COLLECTION_ID_KEY: new_collection_id\n        })\n\n\nclass UploadExploration(base.BaseHandler):\n    \"\"\"Uploads a new exploration.\"\"\"\n\n    @base.require_fully_signed_up\n    def post(self):\n        \"\"\"Handles POST requests.\"\"\"\n        yaml_content = self.request.get('yaml_file')\n\n        new_exploration_id = exp_services.get_new_exploration_id()\n        if feconf.ALLOW_YAML_FILE_UPLOAD:\n            exp_services.save_new_exploration_from_yaml_and_assets(\n                self.user_id, yaml_content, new_exploration_id, [])\n            self.render_json({\n                EXPLORATION_ID_KEY: new_exploration_id\n            })\n        else:\n            raise self.InvalidInputException(\n                'This server does not allow file uploads.')\n\n\nclass DashboardRedirectPage(base.BaseHandler):\n    \"\"\"An page that redirects to the main Dashboard page.\"\"\"\n\n    def get(self):\n        \"\"\"Handles GET requests.\"\"\"\n        self.redirect(feconf.DASHBOARD_URL)\n", "path": "core/controllers/dashboard.py"}], "after_files": [{"content": "# Copyright 2014 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 the creator dashboard, notifications, and creating new\nactivities.\n\"\"\"\n\nfrom core.controllers import base\nfrom core.domain import collection_domain\nfrom core.domain import collection_services\nfrom core.domain import config_domain\nfrom core.domain import exp_domain\nfrom core.domain import exp_services\nfrom core.domain import feedback_services\nfrom core.domain import stats_services\nfrom core.domain import subscription_services\nfrom core.domain import summary_services\nfrom core.domain import user_jobs_continuous\nfrom core.domain import user_services\nimport feconf\nimport utils\n\nEXPLORATION_ID_KEY = 'explorationId'\nCOLLECTION_ID_KEY = 'collectionId'\n\nDEFAULT_TWITTER_SHARE_MESSAGE_DASHBOARD = config_domain.ConfigProperty(\n    'default_twitter_share_message_dashboard', {\n        'type': 'unicode',\n    },\n    'Default text for the Twitter share message for the dashboard',\n    default_value=(\n        'Check out this interactive lesson I created on Oppia - a free '\n        'platform for teaching and learning!'))\n\n\nclass NotificationsDashboardPage(base.BaseHandler):\n    \"\"\"Page with notifications for the user.\"\"\"\n\n    @base.require_user\n    def get(self):\n        if self.username in config_domain.BANNED_USERNAMES.value:\n            raise self.UnauthorizedUserException(\n                'You do not have the credentials to access this page.')\n        elif user_services.has_fully_registered(self.user_id):\n            self.values.update({\n                'meta_description': feconf.DASHBOARD_PAGE_DESCRIPTION,\n                'nav_mode': feconf.NAV_MODE_DASHBOARD,\n            })\n            self.render_template(\n                'dashboard/notifications_dashboard.html',\n                redirect_url_on_logout='/')\n        else:\n            self.redirect(utils.set_url_query_parameter(\n                feconf.SIGNUP_URL, 'return_url', '/notifications_dashboard'))\n\n\nclass NotificationsDashboardHandler(base.BaseHandler):\n    \"\"\"Provides data for the user notifications dashboard.\"\"\"\n\n    def get(self):\n        \"\"\"Handles GET requests.\"\"\"\n        if self.user_id is None:\n            raise self.PageNotFoundException\n\n        job_queued_msec, recent_notifications = (\n            user_jobs_continuous.DashboardRecentUpdatesAggregator.get_recent_notifications(  # pylint: disable=line-too-long\n                self.user_id))\n\n        last_seen_msec = (\n            subscription_services.get_last_seen_notifications_msec(\n                self.user_id))\n\n        # Replace author_ids with their usernames.\n        author_ids = [\n            notification['author_id'] for notification in recent_notifications\n            if notification['author_id']]\n        author_usernames = user_services.get_usernames(author_ids)\n\n        author_id_to_username = {\n            None: '',\n        }\n        for ind, author_id in enumerate(author_ids):\n            author_id_to_username[author_id] = author_usernames[ind]\n        for notification in recent_notifications:\n            notification['author_username'] = (\n                author_id_to_username[notification['author_id']])\n            del notification['author_id']\n\n        subscription_services.record_user_has_seen_notifications(\n            self.user_id, job_queued_msec if job_queued_msec else 0.0)\n\n        self.values.update({\n            # This may be None if no job has ever run for this user.\n            'job_queued_msec': job_queued_msec,\n            # This may be None if this is the first time the user has seen\n            # the dashboard.\n            'last_seen_msec': last_seen_msec,\n            'recent_notifications': recent_notifications,\n        })\n        self.render_json(self.values)\n\n\nclass DashboardPage(base.BaseHandler):\n    \"\"\"Page showing the user's creator dashboard.\"\"\"\n\n    @base.require_user\n    def get(self):\n        if self.username in config_domain.BANNED_USERNAMES.value:\n            raise self.UnauthorizedUserException(\n                'You do not have the credentials to access this page.')\n        elif user_services.has_fully_registered(self.user_id):\n            self.values.update({\n                'nav_mode': feconf.NAV_MODE_DASHBOARD,\n                'allow_yaml_file_upload': feconf.ALLOW_YAML_FILE_UPLOAD,\n                'DEFAULT_TWITTER_SHARE_MESSAGE_DASHBOARD': (\n                    DEFAULT_TWITTER_SHARE_MESSAGE_DASHBOARD.value)\n            })\n            self.render_template(\n                'dashboard/dashboard.html', redirect_url_on_logout='/')\n        else:\n            self.redirect(utils.set_url_query_parameter(\n                feconf.SIGNUP_URL, 'return_url', feconf.DASHBOARD_URL))\n\n\nclass DashboardHandler(base.BaseHandler):\n    \"\"\"Provides data for the user's creator dashboard page.\"\"\"\n\n    def get(self):\n        \"\"\"Handles GET requests.\"\"\"\n        if self.user_id is None:\n            raise self.PageNotFoundException\n\n        def _get_intro_card_color(category):\n            return (\n                feconf.CATEGORIES_TO_COLORS[category] if\n                category in feconf.CATEGORIES_TO_COLORS else\n                feconf.DEFAULT_COLOR)\n\n        def _round_average_ratings(rating):\n            return round(rating, feconf.AVERAGE_RATINGS_DASHBOARD_PRECISION)\n\n        exploration_ids_subscribed_to = (\n            subscription_services.get_exploration_ids_subscribed_to(\n                self.user_id))\n\n        subscribed_exploration_summaries = filter(None, (\n            exp_services.get_exploration_summaries_matching_ids(\n                exploration_ids_subscribed_to)))\n        subscribed_collection_summaries = filter(None, (\n            collection_services.get_collection_summaries_matching_ids(\n                subscription_services.get_collection_ids_subscribed_to(\n                    self.user_id))))\n\n        exp_summary_list = summary_services.get_displayable_exp_summary_dicts(\n            subscribed_exploration_summaries)\n        collection_summary_list = []\n\n        feedback_thread_analytics = (\n            feedback_services.get_thread_analytics_multi(\n                exploration_ids_subscribed_to))\n\n        unresolved_answers_dict = (\n            stats_services.get_exps_unresolved_answers_count_for_default_rule(\n                exploration_ids_subscribed_to))\n\n        for ind, exploration in enumerate(exp_summary_list):\n            exploration.update(feedback_thread_analytics[ind].to_dict())\n            exploration.update({\n                'num_unresolved_answers': (\n                    unresolved_answers_dict[exploration['id']]\n                    if exploration['id'] in unresolved_answers_dict else 0\n                )\n            })\n\n        exp_summary_list = sorted(\n            exp_summary_list,\n            key=lambda x: (x['num_open_threads'], x['last_updated_msec']),\n            reverse=True)\n\n        if (self.username in\n                config_domain.WHITELISTED_COLLECTION_EDITOR_USERNAMES.value):\n            for collection_summary in subscribed_collection_summaries:\n                # TODO(sll): Reuse _get_displayable_collection_summary_dicts()\n                # in summary_services, instead of replicating it like this.\n                collection_summary_list.append({\n                    'id': collection_summary.id,\n                    'title': collection_summary.title,\n                    'category': collection_summary.category,\n                    'objective': collection_summary.objective,\n                    'language_code': collection_summary.language_code,\n                    'last_updated': utils.get_time_in_millisecs(\n                        collection_summary.collection_model_last_updated),\n                    'created_on': utils.get_time_in_millisecs(\n                        collection_summary.collection_model_created_on),\n                    'status': collection_summary.status,\n                    'node_count': collection_summary.node_count,\n                    'community_owned': collection_summary.community_owned,\n                    'thumbnail_icon_url': (\n                        utils.get_thumbnail_icon_url_for_category(\n                            collection_summary.category)),\n                    'thumbnail_bg_color': utils.get_hex_color_for_category(\n                        collection_summary.category),\n                })\n\n        dashboard_stats = (\n            user_jobs_continuous.UserStatsAggregator.get_dashboard_stats(\n                self.user_id))\n        dashboard_stats.update({\n            'total_open_feedback': feedback_services.get_total_open_threads(\n                feedback_thread_analytics)\n        })\n        if dashboard_stats and dashboard_stats.get('average_ratings'):\n            dashboard_stats['average_ratings'] = (\n                _round_average_ratings(dashboard_stats['average_ratings']))\n\n        last_week_stats = (\n            user_services.get_last_week_dashboard_stats(self.user_id))\n        if last_week_stats and last_week_stats.get('average_ratings'):\n            last_week_stats['average_ratings'] = (\n                _round_average_ratings(last_week_stats['average_ratings']))\n\n        self.values.update({\n            'explorations_list': exp_summary_list,\n            'collections_list': collection_summary_list,\n            'dashboard_stats': dashboard_stats,\n            'last_week_stats': last_week_stats\n        })\n        self.render_json(self.values)\n\n\nclass NotificationsHandler(base.BaseHandler):\n    \"\"\"Provides data about unseen notifications.\"\"\"\n\n    def get(self):\n        \"\"\"Handles GET requests.\"\"\"\n        num_unseen_notifications = 0\n        if self.user_id and self.username:\n            last_seen_msec = (\n                subscription_services.get_last_seen_notifications_msec(\n                    self.user_id))\n            _, recent_notifications = (\n                user_jobs_continuous.DashboardRecentUpdatesAggregator.get_recent_notifications( # pylint: disable=line-too-long\n                    self.user_id))\n            for notification in recent_notifications:\n                if (notification['last_updated_ms'] > last_seen_msec and\n                        notification['author_id'] != self.user_id):\n                    num_unseen_notifications += 1\n\n        self.render_json({\n            'num_unseen_notifications': num_unseen_notifications,\n        })\n\n\nclass NewExploration(base.BaseHandler):\n    \"\"\"Creates a new exploration.\"\"\"\n\n    @base.require_fully_signed_up\n    def post(self):\n        \"\"\"Handles POST requests.\"\"\"\n        title = self.payload.get('title', feconf.DEFAULT_EXPLORATION_TITLE)\n\n        new_exploration_id = exp_services.get_new_exploration_id()\n        exploration = exp_domain.Exploration.create_default_exploration(\n            new_exploration_id, title=title)\n        exp_services.save_new_exploration(self.user_id, exploration)\n\n        self.render_json({\n            EXPLORATION_ID_KEY: new_exploration_id\n        })\n\n\nclass NewCollection(base.BaseHandler):\n    \"\"\"Creates a new collection.\"\"\"\n\n    @base.require_fully_signed_up\n    def post(self):\n        \"\"\"Handles POST requests.\"\"\"\n        new_collection_id = collection_services.get_new_collection_id()\n        collection = collection_domain.Collection.create_default_collection(\n            new_collection_id)\n        collection_services.save_new_collection(self.user_id, collection)\n\n        self.render_json({\n            COLLECTION_ID_KEY: new_collection_id\n        })\n\n\nclass UploadExploration(base.BaseHandler):\n    \"\"\"Uploads a new exploration.\"\"\"\n\n    @base.require_fully_signed_up\n    def post(self):\n        \"\"\"Handles POST requests.\"\"\"\n        yaml_content = self.request.get('yaml_file')\n\n        new_exploration_id = exp_services.get_new_exploration_id()\n        if feconf.ALLOW_YAML_FILE_UPLOAD:\n            exp_services.save_new_exploration_from_yaml_and_assets(\n                self.user_id, yaml_content, new_exploration_id, [])\n            self.render_json({\n                EXPLORATION_ID_KEY: new_exploration_id\n            })\n        else:\n            raise self.InvalidInputException(\n                'This server does not allow file uploads.')\n\n\nclass DashboardRedirectPage(base.BaseHandler):\n    \"\"\"An page that redirects to the main Dashboard page.\"\"\"\n\n    def get(self):\n        \"\"\"Handles GET requests.\"\"\"\n        self.redirect(feconf.DASHBOARD_URL)\n", "path": "core/controllers/dashboard.py"}]}