From 2d369490dd6e5b6cc0214d8bec787e04ce0d26d9 Mon Sep 17 00:00:00 2001
From: adeelehsan <adeel.ehsan@arbisoft.com>
Date: Tue, 7 May 2019 14:32:24 +0500
Subject: [PATCH] Flag added to load limited courses on Dashboard

Dashboard is set to load 250 courses instead of
all the courses. A flag is also added to change
the number the courses to load.

PROD-204
---
 .../0022_indexing_in_courseenrollment.py      | 19 ++++++++++
 common/djangoapps/student/models.py           | 18 +++++++--
 .../student/tests/test_course_listing.py      | 19 +++++++++-
 common/djangoapps/student/tests/test_views.py | 17 +++++++++
 common/djangoapps/student/views/dashboard.py  | 38 +++++++++++++++++--
 lms/envs/production.py                        |  4 ++
 lms/envs/test.py                              |  4 ++
 lms/templates/dashboard.html                  |  8 +++-
 themes/edx.org/lms/templates/dashboard.html   |  7 ++++
 9 files changed, 124 insertions(+), 10 deletions(-)
 create mode 100644 common/djangoapps/student/migrations/0022_indexing_in_courseenrollment.py

diff --git a/common/djangoapps/student/migrations/0022_indexing_in_courseenrollment.py b/common/djangoapps/student/migrations/0022_indexing_in_courseenrollment.py
new file mode 100644
index 00000000000..8b313ff544f
--- /dev/null
+++ b/common/djangoapps/student/migrations/0022_indexing_in_courseenrollment.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-06-24 19:01
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('student', '0021_historicalcourseenrollment'),
+    ]
+
+    operations = [
+        migrations.AddIndex(
+            model_name='courseenrollment',
+            index=models.Index(fields=[b'user', b'-created'], name='student_cou_user_id_b19dcd_idx'),
+        ),
+    ]
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 2a5fd003188..0fa401d5a2c 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -31,7 +31,7 @@ from django.contrib.auth.signals import user_logged_in, user_logged_out
 from django.core.cache import cache
 from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
 from django.db import IntegrityError, models
-from django.db.models import Count, Q
+from django.db.models import Count, Q, Index
 from django.db.models.signals import post_save, pre_save
 from django.db.utils import ProgrammingError
 from django.dispatch import receiver
@@ -1147,7 +1147,8 @@ class CourseEnrollment(models.Model):
     MODE_CACHE_NAMESPACE = u'CourseEnrollment.mode_and_active'
 
     class Meta(object):
-        unique_together = (('user', 'course'),)
+        unique_together = (('user', 'course'), )
+        indexes = [Index(fields=['user', '-created'])]
         ordering = ('user', 'course')
 
     def __init__(self, *args, **kwargs):
@@ -1610,7 +1611,7 @@ class CourseEnrollment(models.Model):
         return cls.objects.filter(user=user, is_active=1).select_related('user')
 
     @classmethod
-    def enrollments_for_user_with_overviews_preload(cls, user):  # pylint: disable=invalid-name
+    def enrollments_for_user_with_overviews_preload(cls, user, courses_limit=None):  # pylint: disable=invalid-name
         """
         List of user's CourseEnrollments, CourseOverviews preloaded if possible.
 
@@ -1625,7 +1626,12 @@ class CourseEnrollment(models.Model):
         The name of this method is long, but was the end result of hashing out a
         number of alternatives, so pylint can stuff it (disable=invalid-name)
         """
-        enrollments = list(cls.enrollments_for_user(user))
+
+        if courses_limit:
+            enrollments = cls.enrollments_for_user(user).order_by('-created')[:courses_limit]
+        else:
+            enrollments = cls.enrollments_for_user(user)
+
         overviews = CourseOverview.get_from_ids_if_exists(
             enrollment.course_id for enrollment in enrollments
         )
@@ -2400,6 +2406,10 @@ def enforce_single_login(sender, request, user, signal, **kwargs):    # pylint:
 
 class DashboardConfiguration(ConfigurationModel):
     """
+    Note:
+        This model is deprecated and we should not be adding new content to it.
+        We will eventually migrate this one entry to a django setting as well.
+
     Dashboard Configuration settings.
 
     Includes configuration options for the dashboard, which impact behavior and rendering for the application.
diff --git a/common/djangoapps/student/tests/test_course_listing.py b/common/djangoapps/student/tests/test_course_listing.py
index 90405cb480f..be9b13a8fb6 100644
--- a/common/djangoapps/student/tests/test_course_listing.py
+++ b/common/djangoapps/student/tests/test_course_listing.py
@@ -13,7 +13,7 @@ from django.test.client import Client
 from milestones.tests.utils import MilestonesTestCaseMixin
 
 from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
-from student.models import CourseEnrollment
+from student.models import CourseEnrollment, DashboardConfiguration
 from student.roles import GlobalStaff
 from student.tests.factories import UserFactory
 from student.views import get_course_enrollments
@@ -85,6 +85,23 @@ class TestCourseListing(ModuleStoreTestCase, MilestonesTestCaseMixin):
         courses_list = list(get_course_enrollments(self.student, None, []))
         self.assertEqual(len(courses_list), 0)
 
+    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+    def test_get_limited_number_of_courses_using_config(self):
+        course_location = self.store.make_course_key('Org0', 'Course0', 'Run0')
+        self._create_course_with_access_groups(course_location)
+
+        course_location = self.store.make_course_key('Org1', 'Course1', 'Run1')
+        self._create_course_with_access_groups(course_location)
+
+        # get dashboard
+        courses_list = list(get_course_enrollments(self.student, None, []))
+        self.assertEqual(len(courses_list), 2)
+
+        with self.settings(DASHBOARD_COURSE_LIMIT=1):
+            course_limit = settings.DASHBOARD_COURSE_LIMIT
+            courses_list = list(get_course_enrollments(self.student, None, [], course_limit))
+            self.assertEqual(len(courses_list), 1)
+
     def test_errored_course_regular_access(self):
         """
         Test the course list for regular staff when get_course returns an ErrorDescriptor
diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py
index 42e34a54e91..8da4372b04c 100644
--- a/common/djangoapps/student/tests/test_views.py
+++ b/common/djangoapps/student/tests/test_views.py
@@ -573,6 +573,23 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
             self.assertIn('You are not enrolled in any courses yet.', response.content)
             self.assertIn(empty_dashboard_message, response.content)
 
+    @patch('django.conf.settings.DASHBOARD_COURSE_LIMIT', 1)
+    def test_course_limit_on_dashboard(self):
+        course = CourseFactory.create()
+        CourseEnrollmentFactory(
+            user=self.user,
+            course_id=course.id
+        )
+
+        course_v1 = CourseFactory.create()
+        CourseEnrollmentFactory(
+            user=self.user,
+            course_id=course_v1.id
+        )
+
+        response = self.client.get(reverse('dashboard'))
+        self.assertIn('1 results successfully populated', response.content)
+
     @staticmethod
     def _remove_whitespace_from_html_string(html):
         return ''.join(html.split())
diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py
index 398e3c5e8d1..9a8d844e0a0 100644
--- a/common/djangoapps/student/views/dashboard.py
+++ b/common/djangoapps/student/views/dashboard.py
@@ -198,7 +198,7 @@ def _create_recent_enrollment_message(course_enrollments, course_modes):  # pyli
         )
 
 
-def get_course_enrollments(user, org_whitelist, org_blacklist):
+def get_course_enrollments(user, org_whitelist, org_blacklist, course_limit=None):
     """
     Given a user, return a filtered set of his or her course enrollments.
 
@@ -206,12 +206,13 @@ def get_course_enrollments(user, org_whitelist, org_blacklist):
         user (User): the user in question.
         org_whitelist (list[str]): If not None, ONLY courses of these orgs will be returned.
         org_blacklist (list[str]): Courses of these orgs will be excluded.
+        course_limit: Number courses to load in dashboard if set to None then all the courses would be load.
 
     Returns:
         generator[CourseEnrollment]: a sequence of enrollments to be displayed
         on the user's dashboard.
     """
-    for enrollment in CourseEnrollment.enrollments_for_user_with_overviews_preload(user):
+    for enrollment in CourseEnrollment.enrollments_for_user_with_overviews_preload(user, course_limit):
 
         # If the course is missing or broken, log an error and skip it.
         course_overview = enrollment.course_overview
@@ -526,6 +527,29 @@ def _credit_statuses(user, course_enrollments):
     return statuses
 
 
+def show_load_all_courses_link(user, course_limit, course_enrollments):
+    """
+    By default dashboard will show limited courses based on the course limit
+    set in configuration.
+
+    A link would be provided provided at the bottom to load all the courses if there are any courses.
+    """
+
+    if course_limit is None:
+        return False
+
+    total_enrollments = CourseEnrollment.enrollments_for_user(user).count()
+    return len(course_enrollments) < total_enrollments
+
+
+def get_dashboard_course_limit():
+    """
+    get course limit from configuration
+    """
+    course_limit = getattr(settings, 'DASHBOARD_COURSE_LIMIT', None)
+    return course_limit
+
+
 @login_required
 @ensure_csrf_cookie
 @add_maintenance_banner
@@ -534,7 +558,9 @@ def student_dashboard(request):
     Provides the LMS dashboard view
 
     TODO: This is lms specific and does not belong in common code.
-
+    Note:
+        To load the all courses set course_limit=None as parameter in GET. If its not None then default course
+        limit will be used  that is set in configuration
     Arguments:
         request: The request object.
 
@@ -567,9 +593,12 @@ def student_dashboard(request):
         'EMPTY_DASHBOARD_MESSAGE', None
     )
 
+    disable_course_limit = request and 'course_limit' in request.GET
+    course_limit = get_dashboard_course_limit() if not disable_course_limit else None
+
     # Get the org whitelist or the org blacklist for the current site
     site_org_whitelist, site_org_blacklist = get_org_black_and_whitelist_for_site()
-    course_enrollments = list(get_course_enrollments(user, site_org_whitelist, site_org_blacklist))
+    course_enrollments = list(get_course_enrollments(user, site_org_whitelist, site_org_blacklist, course_limit))
 
     # Get the entitlements for the user and a mapping to all available sessions for that entitlement
     # If an entitlement has no available sessions, pass through a mock course overview object
@@ -854,6 +883,7 @@ def student_dashboard(request):
         'empty_dashboard_message': empty_dashboard_message,
         'recovery_email_message': recovery_email_message,
         'recovery_email_activation_message': recovery_email_activation_message,
+        'show_load_all_courses_link': show_load_all_courses_link(user, course_limit, course_enrollments),
         # TODO START: clean up as part of REVEM-199 (START)
         'course_info': get_dashboard_course_info(user, course_enrollments),
         # TODO START: clean up as part of REVEM-199 (END)
diff --git a/lms/envs/production.py b/lms/envs/production.py
index b07e245ba9f..ef260dad230 100644
--- a/lms/envs/production.py
+++ b/lms/envs/production.py
@@ -1132,3 +1132,7 @@ derive_settings(__name__)
 ######################### Overriding custom enrollment roles ###############
 
 MANUAL_ENROLLMENT_ROLE_CHOICES = ENV_TOKENS.get('MANUAL_ENROLLMENT_ROLE_CHOICES', MANUAL_ENROLLMENT_ROLE_CHOICES)
+
+########################## limiting dashboard courses ######################
+
+DASHBOARD_COURSE_LIMIT = ENV_TOKENS.get('DASHBOARD_COURSE_LIMIT', None)
diff --git a/lms/envs/test.py b/lms/envs/test.py
index 504cb49d839..2edc04df502 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -650,3 +650,7 @@ PDF_RECEIPT_TAX_ID_LABEL = 'Tax ID'
 PROFILE_MICROFRONTEND_URL = "http://profile-mfe/abc/"
 ORDER_HISTORY_MICROFRONTEND_URL = "http://order-history-mfe/"
 ACCOUNT_MICROFRONTEND_URL = "http://account-mfe/"
+
+########################## limiting dashboard courses ######################
+
+DASHBOARD_COURSE_LIMIT = 250
diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html
index a6db6735831..6451c814b09 100644
--- a/lms/templates/dashboard.html
+++ b/lms/templates/dashboard.html
@@ -206,7 +206,13 @@ from student.models import CourseEnrollment
               %>
               <%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_refund_entitlement=can_refund_entitlement, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name, resume_button_url=resume_button_url' />
             % endfor
-
+            % if show_load_all_courses_link:
+                <br/>
+                 ${len(course_enrollments)} ${_("results successfully populated,")}
+                 <a href="${reverse('dashboard')}?course_limit=None">
+                     ${_("Click to load all enrolled courses")}
+                </a>
+            % endif
             </ul>
           % else:
             <div class="empty-dashboard-message">
diff --git a/themes/edx.org/lms/templates/dashboard.html b/themes/edx.org/lms/templates/dashboard.html
index 021ab1f3cbc..c582fc49e93 100644
--- a/themes/edx.org/lms/templates/dashboard.html
+++ b/themes/edx.org/lms/templates/dashboard.html
@@ -203,6 +203,13 @@ from student.models import CourseEnrollment
           %>
           <%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_refund_entitlement=can_refund_entitlement, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name, resume_button_url=resume_button_url' />
         % endfor
+        % if show_load_all_courses_link:
+                <br/>
+                 ${len(course_enrollments)} ${_("results successfully populated,")}
+                 <a href="${reverse('dashboard')}?course_limit=None">
+                     ${_("Click to load all enrolled courses")}
+                </a>
+            % endif
         </ul>
     % else:
       <section class="empty-dashboard-message">
-- 
GitLab