diff --git a/core/controllers/home.py b/core/controllers/home.py index 3d79cc184304..ad63490bfc1a 100644 --- a/core/controllers/home.py +++ b/core/controllers/home.py @@ -130,19 +130,17 @@ def get(self): exp_services.get_at_least_editable_exploration_summaries( self.user_id)) - threads = {} - for exp_summary in editable_exp_summaries.values(): - threads[exp_summary.id] = feedback_services.get_thread_count( - exp_summary.id) - def _get_intro_card_color(category): return ( feconf.CATEGORIES_TO_COLORS[category] if category in feconf.CATEGORIES_TO_COLORS else feconf.DEFAULT_COLOR) - self.values.update({ - 'explorations_list': [{ + explorations_list = [] + for exp_summary in editable_exp_summaries.values(): + feedback_thread_analytics = feedback_services.get_thread_analytics( + exp_summary.id) + explorations_list.append({ 'id': exp_summary.id, 'title': exp_summary.title, 'category': exp_summary.category, @@ -159,12 +157,19 @@ def _get_intro_card_color(category): '/images/gallery/exploration_background_%s_small.png' % _get_intro_card_color(exp_summary.category)), 'ratings': exp_summary.ratings, - } for exp_summary in editable_exp_summaries.values()], - 'thread_count_list': [{ - 'id': exp_id, - 'open_threads': threads[exp_id][0] if threads[exp_id] else 0, - 'total_threads': threads[exp_id][1] if threads[exp_id] else 0, - } for exp_id in threads.keys()], + 'num_open_threads': ( + feedback_thread_analytics['num_open_threads']), + 'num_total_threads': ( + feedback_thread_analytics['num_total_threads']), + }) + + explorations_list = sorted( + explorations_list, + key=lambda x: (x['num_open_threads'], x['last_updated']), + reverse=True) + + self.values.update({ + 'explorations_list': explorations_list, }) self.render_json(self.values) diff --git a/core/domain/feedback_jobs.py b/core/domain/feedback_jobs.py index 1b30a91406cf..c6c85ff4a0f3 100644 --- a/core/domain/feedback_jobs.py +++ b/core/domain/feedback_jobs.py @@ -22,36 +22,35 @@ models.NAMES.base_model, models.NAMES.feedback, models.NAMES.exploration ]) transaction_services = models.Registry.import_transaction_services() -import feconf -import utils -from google.appengine.ext import ndb - -class OpenFeedbacksRealtimeModel( +class FeedbackAnalyticsRealtimeModel( jobs.BaseRealtimeDatastoreClassForContinuousComputations): pass -class OpenFeedbacksStatisticsAggregator(jobs.BaseContinuousComputationManager): - """A continuous-computation job that computes the number of open feedbacks - for explorations. +class FeedbackAnalyticsAggregator(jobs.BaseContinuousComputationManager): + """A continuous-computation job that computes analytics for feedback + threads of explorations. This job does not have a realtime component. There will be a delay in propagating new updates to the dashboard; the length of the delay will be approximately the time it takes a batch job to run. """ + # TODO(sll): Add a realtime component: listen to incoming feedback messages + # and adjust the counts appropriately. + @classmethod def get_event_types_listened_to(cls): return [] @classmethod def _get_realtime_datastore_class(cls): - return OpenFeedbacksRealtimeModel + return FeedbackAnalyticsRealtimeModel @classmethod def _get_batch_job_manager_class(cls): - return OpenFeedbacksMRJobManager + return FeedbackAnalyticsMRJobManager @classmethod def _handle_incoming_event(cls, active_realtime_layer, event_type, *args): @@ -59,32 +58,44 @@ def _handle_incoming_event(cls, active_realtime_layer, event_type, *args): # Public query methods. @classmethod - def get_thread_count(cls, exploration_id): + def get_thread_analytics(cls, exploration_id): """ Args: - - exploration_id: id of the exploration to get statistics for + - exploration_id: id of the exploration to get statistics for. - Returns the number of open feedbacks. + Returns a dict with two keys: 'num_open_threads' and + 'num_total_threads', representing the counts of open and all feedback + threads, respectively. """ + feedback_thread_analytics_model = ( + feedback_models.FeedbackAnalyticsModel.get( + exploration_id, strict=False)) + + num_open_threads = 0 + num_total_threads = 0 + if feedback_thread_analytics_model: + num_open_threads = feedback_thread_analytics_model.num_open_threads + num_total_threads = ( + feedback_thread_analytics_model.num_total_threads) - feedback_thread_analytics_model = feedback_models.OpenFeedbacksModel.get( - exploration_id, strict=False) - return ([feedback_thread_analytics_model.num_of_open_threads, - feedback_thread_analytics_model.total_threads] - if feedback_thread_analytics_model else None) + return { + 'num_open_threads': num_open_threads, + 'num_total_threads': num_total_threads + } -class OpenFeedbacksMRJobManager( +class FeedbackAnalyticsMRJobManager( jobs.BaseMapReduceJobManagerForContinuousComputations): - """Job that creates OpenFeedbackModels for explorations by calculating the - number of open feedback threads per exploration. - Includes: - * number of open feedbacks for an exploration. + """Job that creates FeedbackAnalyticsModels for explorations by calculating + various analytics for feedback threads corresponding to an exploration. + + Currently, this job calculates the number of open feedback threads, as well + as the total feedback thread count for each exploration. """ @classmethod def _get_continuous_computation_class(cls): - return OpenFeedbacksStatisticsAggregator + return FeedbackAnalyticsAggregator @classmethod def entity_classes_to_map_over(cls): @@ -92,14 +103,13 @@ def entity_classes_to_map_over(cls): @staticmethod def map(item): - if (item.status == feedback_models.STATUS_CHOICES_OPEN): - yield (item.exploration_id, 1) - else: - yield (item.exploration_id, 0) + yield (item.exploration_id, item.status) @staticmethod def reduce(key, stringified_values): - open_threads = stringified_values.count('1') - total_threads = open_threads + stringified_values.count('0') - feedback_models.OpenFeedbacksModel.create(key, open_threads, - total_threads) + num_open_threads = stringified_values.count( + feedback_models.STATUS_CHOICES_OPEN) + num_total_threads = len(stringified_values) + + feedback_models.FeedbackAnalyticsModel.create( + key, num_open_threads, num_total_threads) diff --git a/core/domain/feedback_jobs_test.py b/core/domain/feedback_jobs_test.py index bca01d19e25d..ad1367a61cf1 100644 --- a/core/domain/feedback_jobs_test.py +++ b/core/domain/feedback_jobs_test.py @@ -14,42 +14,42 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for open feedback threads continuous computations.""" +"""Tests for continuous computations relating to feedback analytics.""" from core import jobs_registry -from core.domain import event_services from core.domain import feedback_jobs from core.platform import models (feedback_models,) = models.Registry.import_models([models.NAMES.feedback]) from core.tests import test_utils -import feconf -class ModifiedOpenFeedbacksAggregator(feedback_jobs.OpenFeedbacksStatisticsAggregator): - """A modified OpenFeedbacksStatisticsAggregator that does not start a new batch +class ModifiedFeedbackAnalyticsAggregator( + feedback_jobs.FeedbackAnalyticsAggregator): + """A modified FeedbackAnalyticsAggregator that does not start a new batch job when the previous one has finished. """ @classmethod def _get_batch_job_manager_class(cls): - return ModifiedOpenFeedbacksMRJobManager + return ModifiedFeedbackAnalyticsMRJobManager @classmethod def _kickoff_batch_job_after_previous_one_ends(cls): pass -class ModifiedOpenFeedbacksMRJobManager(feedback_jobs.OpenFeedbacksMRJobManager): +class ModifiedFeedbackAnalyticsMRJobManager( + feedback_jobs.FeedbackAnalyticsMRJobManager): @classmethod def _get_continuous_computation_class(cls): - return ModifiedOpenFeedbacksAggregator + return ModifiedFeedbackAnalyticsAggregator -class OpenFeedbacksAggregatorUnitTests(test_utils.GenericTestBase): +class FeedbackAnalyticsAggregatorUnitTests(test_utils.GenericTestBase): """Tests for statistics aggregations.""" ALL_CONTINUOUS_COMPUTATION_MANAGERS_FOR_TESTS = [ - ModifiedOpenFeedbacksAggregator] + ModifiedFeedbackAnalyticsAggregator] def test_no_threads(self): with self.swap( @@ -58,16 +58,19 @@ def test_no_threads(self): # Create test objects. exp_id = 'eid' self.save_new_valid_exploration(exp_id, 'owner') - + # Start job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id)) - self.assertIsNone(threads) + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id), { + 'num_open_threads': 0, + 'num_total_threads': 0, + }) def test_single_thread_single_exp(self): with self.swap( @@ -81,17 +84,19 @@ def test_single_thread_single_exp(self): exp_id, thread_id) thread.exploration_id = exp_id thread.put() - + # Start job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id)) - self.assertEqual(threads[0], 1) - self.assertEqual(threads[1], 1) + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id), { + 'num_open_threads': 1, + 'num_total_threads': 1, + }) def test_multiple_threads_single_exp(self): with self.swap( @@ -108,19 +113,21 @@ def test_multiple_threads_single_exp(self): thread_1.put() thread_2 = feedback_models.FeedbackThreadModel.create( exp_id, thread_id_2) - thread_2.exploration_id = exp_id + thread_2.exploration_id = exp_id thread_2.put() - + # Start job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id)) - self.assertEqual(threads[0], 2) - self.assertEqual(threads[1], 2) + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id), { + 'num_open_threads': 2, + 'num_total_threads': 2, + }) def test_multiple_threads_multiple_exp(self): with self.swap( @@ -141,7 +148,7 @@ def test_multiple_threads_multiple_exp(self): thread_1.put() thread_2 = feedback_models.FeedbackThreadModel.create( exp_id_1, thread_id_2) - thread_2.exploration_id = exp_id_1 + thread_2.exploration_id = exp_id_1 thread_2.put() thread_3 = feedback_models.FeedbackThreadModel.create( exp_id_2, thread_id_3) @@ -149,25 +156,29 @@ def test_multiple_threads_multiple_exp(self): thread_3.put() thread_4 = feedback_models.FeedbackThreadModel.create( exp_id_2, thread_id_4) - thread_4.exploration_id = exp_id_2 + thread_4.exploration_id = exp_id_2 thread_4.put() - + # Start job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id_1)) - self.assertEqual(threads[0], 2) - self.assertEqual(threads[1], 2) - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id_2)) - self.assertEqual(threads[0], 2) - self.assertEqual(threads[1], 2) - - def test_thread_closed_job_running(self): + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id_1), { + 'num_open_threads': 2, + 'num_total_threads': 2, + }) + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id_2), { + 'num_open_threads': 2, + 'num_total_threads': 2, + }) + + def test_thread_closed_job_running(self): with self.swap( jobs_registry, 'ALL_CONTINUOUS_COMPUTATION_MANAGERS', self.ALL_CONTINUOUS_COMPUTATION_MANAGERS_FOR_TESTS): @@ -175,26 +186,27 @@ def test_thread_closed_job_running(self): user_id = 'uid' exp_id = 'eid' thread_id_1 = 'tid1' - thread_id_2 = 'tid2' self.save_new_valid_exploration(exp_id, 'owner') thread_1 = feedback_models.FeedbackThreadModel.create( exp_id, thread_id_1) thread_1.exploration_id = exp_id thread_1.put() - + # Start job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id)) - self.assertEqual(threads[0], 1) - self.assertEqual(threads[1], 1) - + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id), { + 'num_open_threads': 1, + 'num_total_threads': 1, + }) + # Stop job. - ModifiedOpenFeedbacksAggregator.stop_computation(user_id) + ModifiedFeedbackAnalyticsAggregator.stop_computation(user_id) self.assertEqual(self.count_jobs_in_taskqueue(), 0) # Close thread. @@ -202,19 +214,21 @@ def test_thread_closed_job_running(self): get_by_exp_and_thread_id(exp_id, thread_id_1)) thread.status = feedback_models.STATUS_CHOICES_FIXED thread.put() - + # Restart job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id)) - self.assertEqual(threads[0], 0) - self.assertEqual(threads[1], 1) - - def test_thread_closed_reopened_again(self): + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id), { + 'num_open_threads': 0, + 'num_total_threads': 1, + }) + + def test_thread_closed_reopened_again(self): with self.swap( jobs_registry, 'ALL_CONTINUOUS_COMPUTATION_MANAGERS', self.ALL_CONTINUOUS_COMPUTATION_MANAGERS_FOR_TESTS): @@ -222,26 +236,27 @@ def test_thread_closed_reopened_again(self): user_id = 'uid' exp_id = 'eid' thread_id_1 = 'tid1' - thread_id_2 = 'tid2' self.save_new_valid_exploration(exp_id, 'owner') thread_1 = feedback_models.FeedbackThreadModel.create( exp_id, thread_id_1) thread_1.exploration_id = exp_id thread_1.put() - + # Start job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id)) - self.assertEqual(threads[0], 1) - self.assertEqual(threads[1], 1) - + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id), { + 'num_open_threads': 1, + 'num_total_threads': 1, + }) + # Stop job. - ModifiedOpenFeedbacksAggregator.stop_computation(user_id) + ModifiedFeedbackAnalyticsAggregator.stop_computation(user_id) self.assertEqual(self.count_jobs_in_taskqueue(), 0) # Close thread. @@ -249,22 +264,24 @@ def test_thread_closed_reopened_again(self): get_by_exp_and_thread_id(exp_id, thread_id_1)) thread.status = feedback_models.STATUS_CHOICES_FIXED thread.put() - + # Restart job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id)) - self.assertEqual(threads[0], 0) - self.assertEqual(threads[1], 1) + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id), { + 'num_open_threads': 0, + 'num_total_threads': 1, + }) # Stop job. - ModifiedOpenFeedbacksAggregator.stop_computation(user_id) + ModifiedFeedbackAnalyticsAggregator.stop_computation(user_id) self.assertEqual(self.count_jobs_in_taskqueue(), 0) - + # Reopen thread. thread = (feedback_models.FeedbackThreadModel. get_by_exp_and_thread_id(exp_id, thread_id_1)) @@ -273,16 +290,18 @@ def test_thread_closed_reopened_again(self): # Restart job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id)) - self.assertEqual(threads[0], 1) - self.assertEqual(threads[1], 1) - - def test_thread_closed_status_changed(self): + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id), { + 'num_open_threads': 1, + 'num_total_threads': 1, + }) + + def test_thread_closed_status_changed(self): with self.swap( jobs_registry, 'ALL_CONTINUOUS_COMPUTATION_MANAGERS', self.ALL_CONTINUOUS_COMPUTATION_MANAGERS_FOR_TESTS): @@ -290,26 +309,27 @@ def test_thread_closed_status_changed(self): user_id = 'uid' exp_id = 'eid' thread_id_1 = 'tid1' - thread_id_2 = 'tid2' self.save_new_valid_exploration(exp_id, 'owner') thread_1 = feedback_models.FeedbackThreadModel.create( exp_id, thread_id_1) thread_1.exploration_id = exp_id thread_1.put() - + # Start job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id)) - self.assertEqual(threads[0], 1) - self.assertEqual(threads[1], 1) - + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id), { + 'num_open_threads': 1, + 'num_total_threads': 1, + }) + # Stop job. - ModifiedOpenFeedbacksAggregator.stop_computation(user_id) + ModifiedFeedbackAnalyticsAggregator.stop_computation(user_id) self.assertEqual(self.count_jobs_in_taskqueue(), 0) # Close thread. @@ -317,22 +337,24 @@ def test_thread_closed_status_changed(self): get_by_exp_and_thread_id(exp_id, thread_id_1)) thread.status = feedback_models.STATUS_CHOICES_FIXED thread.put() - + # Restart job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id)) - self.assertEqual(threads[0], 0) - self.assertEqual(threads[1], 1) + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id), { + 'num_open_threads': 0, + 'num_total_threads': 1, + }) # Stop job. - ModifiedOpenFeedbacksAggregator.stop_computation(user_id) + ModifiedFeedbackAnalyticsAggregator.stop_computation(user_id) self.assertEqual(self.count_jobs_in_taskqueue(), 0) - + # Change thread status. thread = (feedback_models.FeedbackThreadModel. get_by_exp_and_thread_id(exp_id, thread_id_1)) @@ -341,11 +363,13 @@ def test_thread_closed_status_changed(self): # Restart job. self.process_and_flush_pending_tasks() - ModifiedOpenFeedbacksAggregator.start_computation() + ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() - threads = (ModifiedOpenFeedbacksAggregator. - get_thread_count(exp_id)) - self.assertEqual(threads[0], 0) - self.assertEqual(threads[1], 1) + self.assertEqual( + ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( + exp_id), { + 'num_open_threads': 0, + 'num_total_threads': 1, + }) diff --git a/core/domain/feedback_services.py b/core/domain/feedback_services.py index 1225aae036ef..d07fede43a5a 100644 --- a/core/domain/feedback_services.py +++ b/core/domain/feedback_services.py @@ -148,10 +148,14 @@ def get_last_updated_time(exploration_id): ) if threadlist else None -def get_thread_count(exploration_id): - """Returns the number of open feedbacks for this exploration. - - If this exploration has no open feedbacks, returns 0. +def get_thread_analytics(exploration_id): + """Returns a dict with feedback thread analytics for the given exploration. + + The returned dict has two keys: + - 'num_open_threads': the number of open feedback threads for this + exploration. + - 'num_total_threads': the total number of feedback threads for this + exploration. """ - return (feedback_jobs.OpenFeedbacksStatisticsAggregator. - get_thread_count(exploration_id)) + return feedback_jobs.FeedbackAnalyticsAggregator.get_thread_analytics( + exploration_id) diff --git a/core/jobs_registry.py b/core/jobs_registry.py index bcb4da38d857..8b228f1ddedc 100644 --- a/core/jobs_registry.py +++ b/core/jobs_registry.py @@ -39,7 +39,7 @@ exp_jobs.SearchRanker, stats_jobs.StatisticsAggregator, user_jobs.DashboardRecentUpdatesAggregator, - feedback_jobs.OpenFeedbacksStatisticsAggregator] + feedback_jobs.FeedbackAnalyticsAggregator] class ContinuousComputationEventDispatcher(object): diff --git a/core/storage/feedback/gae_models.py b/core/storage/feedback/gae_models.py index 1a2bd13e3042..4434ba9732a4 100644 --- a/core/storage/feedback/gae_models.py +++ b/core/storage/feedback/gae_models.py @@ -205,24 +205,21 @@ def get_all_messages(cls, page_size, urlsafe_start_cursor): cls.query(), page_size, urlsafe_start_cursor) -class OpenFeedbacksModel(base_models.BaseMapReduceBatchResultsModel): - """Model for storing open feedback counts for explorations. - - A OpenFeedbacksModel instance stores the following information: - - id, num_of_open_feedbacks +class FeedbackAnalyticsModel(base_models.BaseMapReduceBatchResultsModel): + """Model for storing feedback thread analytics for an exploration. The key of each instance is the exploration id. """ - - # The number of open feedbacks for an exploration. - num_of_open_threads = ndb.IntegerProperty(default=None, indexed=False) - - # Total number of threads against this exploration. - total_threads = ndb.IntegerProperty(default=None, indexed=False) + # The number of open feedback threads filed against this exploration. + num_open_threads = ndb.IntegerProperty(default=None, indexed=True) + # Total number of feedback threads filed against this exploration. + num_total_threads = ndb.IntegerProperty(default=None, indexed=True) @classmethod - def create(cls, id, open_threads, total_threads): - """Creates a new OpenFeedbacksModel entry.""" - cls(id=id, num_of_open_threads=open_threads, - total_threads=total_threads).put() + def create(cls, id, num_open_threads, num_total_threads): + """Creates a new FeedbackAnalyticsModel entry.""" + cls( + id=id, + num_open_threads=num_open_threads, + num_total_threads=num_total_threads + ).put() diff --git a/core/templates/dev/head/css/oppia.css b/core/templates/dev/head/css/oppia.css index 875ce220403f..eb4465d15eaf 100644 --- a/core/templates/dev/head/css/oppia.css +++ b/core/templates/dev/head/css/oppia.css @@ -248,7 +248,7 @@ div.wysiwyg iframe.wysiwyg-content-large { } .oppia-sidebar-menu-icon { - height: 22px; + height: 22px; margin: 0px 16px 3px 6px; width: 22px; } @@ -1127,7 +1127,8 @@ oppia-parameter { } .oppia-dashboard-status-green { - color: #65e065; + color: #05a69a; + font-weight: bold; text-transform: capitalize; } @@ -1137,7 +1138,8 @@ oppia-parameter { } .oppia-dashboard-status-orange { - color: #ffce00; + color: #f7a541; + font-weight: bold; text-transform: capitalize; } diff --git a/core/templates/dev/head/dashboard/my_explorations.html b/core/templates/dev/head/dashboard/my_explorations.html index f7e7515616e5..981a9220a78b 100644 --- a/core/templates/dev/head/dashboard/my_explorations.html +++ b/core/templates/dev/head/dashboard/my_explorations.html @@ -46,32 +46,39 @@