diff --git a/futurex_openedx_extensions/dashboard/details/courses.py b/futurex_openedx_extensions/dashboard/details/courses.py index 1080a78a..d122afe7 100644 --- a/futurex_openedx_extensions/dashboard/details/courses.py +++ b/futurex_openedx_extensions/dashboard/details/courses.py @@ -15,7 +15,7 @@ def get_courses_queryset( - tenant_ids: List, search_text: str = None, only_visible: bool = True, only_active: bool = False + tenant_ids: List, search_text: str = None, visible_filter: bool = True, active_filter: bool = None ) -> QuerySet: """ Get the courses queryset for the given tenant IDs and search text. @@ -24,16 +24,18 @@ def get_courses_queryset( :type tenant_ids: List :param search_text: Search text to filter the courses by :type search_text: str - :param only_visible: Whether to only include courses that are visible in the catalog - :type only_visible: bool - :param only_active: Whether to only include active courses - :type only_active: bool + :param visible_filter: Whether to only include courses that are visible in the catalog + :type visible_filter: bool + :param active_filter: Whether to only include active courses + :type active_filter: bool :return: QuerySet of courses :rtype: QuerySet """ course_org_filter_list = get_course_org_filter_list(tenant_ids)['course_org_filter_list'] - queryset = get_base_queryset_courses(course_org_filter_list, only_visible=only_visible, only_active=only_active) + queryset = get_base_queryset_courses( + course_org_filter_list, visible_filter=visible_filter, active_filter=active_filter, + ) search_text = (search_text or '').strip() if search_text: diff --git a/futurex_openedx_extensions/dashboard/details/learners.py b/futurex_openedx_extensions/dashboard/details/learners.py index 1a92b05c..b401e134 100644 --- a/futurex_openedx_extensions/dashboard/details/learners.py +++ b/futurex_openedx_extensions/dashboard/details/learners.py @@ -3,17 +3,86 @@ from typing import List -from common.djangoapps.student.models import CourseAccessRole, UserSignupSource +from common.djangoapps.student.models import CourseAccessRole from django.contrib.auth import get_user_model from django.db.models import Count, Exists, OuterRef, Q, Subquery from django.db.models.query import QuerySet -from futurex_openedx_extensions.helpers.querysets import get_base_queryset_courses -from futurex_openedx_extensions.helpers.tenants import get_course_org_filter_list, get_tenant_site +from futurex_openedx_extensions.helpers.querysets import get_base_queryset_courses, get_has_site_login_queryset +from futurex_openedx_extensions.helpers.tenants import get_course_org_filter_list, get_tenants_sites + + +def get_courses_count_for_learner_queryset( + course_org_filter_list: List[str], + visible_courses_filter: bool = True, + active_courses_filter: bool = None, +) -> QuerySet: + """ + Get the courses count for the given learner. + + :param course_org_filter_list: List of course organizations to filter by + :type course_org_filter_list: List[str] + :param visible_courses_filter: Value to filter courses on catalog visibility. None means no filter. + :type visible_courses_filter: bool + :param active_courses_filter: Value to filter courses on active status. None means no filter. + :type active_courses_filter: bool + :return: QuerySet of learners + :rtype: QuerySet + """ + return Count( + 'courseenrollment', + filter=( + Q(courseenrollment__course_id__in=get_base_queryset_courses( + course_org_filter_list, + visible_filter=visible_courses_filter, + active_filter=active_courses_filter, + )) & + ~Exists( + CourseAccessRole.objects.filter( + user_id=OuterRef('id'), + org=OuterRef('courseenrollment__course__org') + ) + ) + ), + distinct=True + ) + + +def get_certificates_count_for_learner_queryset( + course_org_filter_list: List[str], + visible_courses_filter: bool = True, + active_courses_filter: bool = None, +) -> QuerySet: + """ + Annotate the given queryset with the certificate counts. + + :param course_org_filter_list: List of course organizations to filter by + :type course_org_filter_list: List[str] + :param visible_courses_filter: Value to filter courses on catalog visibility. None means no filter. + :type visible_courses_filter: bool + :param active_courses_filter: Value to filter courses on active status. None means no filter. + :type active_courses_filter: bool + :return: QuerySet of learners + :rtype: QuerySet + """ + return Count( + 'generatedcertificate', + filter=( + Q(generatedcertificate__course_id__in=Subquery( + get_base_queryset_courses( + course_org_filter_list, + visible_filter=visible_courses_filter, + active_filter=active_courses_filter + ).values_list('id', flat=True) + )) & + Q(generatedcertificate__status='downloadable') + ), + distinct=True + ) def get_learners_queryset( - tenant_ids: List, search_text: str = None, only_visible_courses: bool = True, only_active_courses: bool = False + tenant_ids: List, search_text: str = None, visible_courses_filter: bool = True, active_courses_filter: bool = None ) -> QuerySet: """ Get the learners queryset for the given tenant IDs and search text. @@ -22,18 +91,15 @@ def get_learners_queryset( :type tenant_ids: List :param search_text: Search text to filter the learners by :type search_text: str - :param only_visible_courses: Whether to only count courses that are visible in the catalog - :type only_visible_courses: bool - :param only_active_courses: Whether to only count active courses - :type only_active_courses: bool + :param visible_courses_filter: Whether to only count courses that are visible in the catalog + :type visible_courses_filter: bool + :param active_courses_filter: Whether to only count active courses + :type active_courses_filter: bool :return: QuerySet of learners :rtype: QuerySet """ course_org_filter_list = get_course_org_filter_list(tenant_ids)['course_org_filter_list'] - tenant_sites = [] - for tenant_id in tenant_ids: - if site := get_tenant_site(tenant_id): - tenant_sites.append(site) + tenant_sites = get_tenants_sites(tenant_ids) queryset = get_user_model().objects.filter( is_superuser=False, @@ -49,45 +115,19 @@ def get_learners_queryset( ) queryset = queryset.annotate( - courses_count=Count( - 'courseenrollment', - filter=( - Q(courseenrollment__course_id__in=get_base_queryset_courses( - course_org_filter_list, - only_visible=only_visible_courses, - only_active=only_active_courses, - )) & - ~Exists( - CourseAccessRole.objects.filter( - user_id=OuterRef('id'), - org=OuterRef('courseenrollment__course__org') - ) - ) - ), - distinct=True + courses_count=get_courses_count_for_learner_queryset( + course_org_filter_list, + visible_courses_filter=visible_courses_filter, + active_courses_filter=active_courses_filter, ) ).annotate( - certificates_count=Count( - 'generatedcertificate', - filter=( - Q(generatedcertificate__course_id__in=Subquery( - get_base_queryset_courses( - course_org_filter_list, - only_visible=only_visible_courses, - only_active=only_active_courses - ).values_list('id', flat=True) - )) & - Q(generatedcertificate__status='downloadable') - ), - distinct=True + certificates_count=get_certificates_count_for_learner_queryset( + course_org_filter_list, + visible_courses_filter=visible_courses_filter, + active_courses_filter=active_courses_filter, ) ).annotate( - has_site_login=Exists( - UserSignupSource.objects.filter( - user_id=OuterRef('id'), - site__in=tenant_sites - ) - ) + has_site_login=get_has_site_login_queryset(tenant_sites) ).filter( Q(courses_count__gt=0) | Q(has_site_login=True) ).select_related('profile').order_by('id') diff --git a/futurex_openedx_extensions/dashboard/statistics/certificates.py b/futurex_openedx_extensions/dashboard/statistics/certificates.py index ffe95644..10a14f84 100644 --- a/futurex_openedx_extensions/dashboard/statistics/certificates.py +++ b/futurex_openedx_extensions/dashboard/statistics/certificates.py @@ -12,7 +12,7 @@ def get_certificates_count( - tenant_ids: List[int], only_visible_courses: bool = True, only_active_courses: bool = False + tenant_ids: List[int], visible_courses_filter: bool = True, active_courses_filter: bool = None ) -> Dict[str, int]: """ Get the count of issued certificates in the given tenants. The count is grouped by organization. Certificates @@ -20,10 +20,10 @@ def get_certificates_count( :param tenant_ids: List of tenant IDs to get the count for :type tenant_ids: List[int] - :param only_visible_courses: Whether to only count courses that are visible in the catalog - :type only_visible_courses: bool - :param only_active_courses: Whether to only count active courses (according to dates) - :type only_active_courses: bool + :param visible_courses_filter: Value to filter courses on catalog visibility. None means no filter. + :type visible_courses_filter: bool + :param active_courses_filter: Value to filter courses on active status. None means no filter. + :type active_courses_filter: bool :return: Count of certificates per organization :rtype: Dict[str, int] """ @@ -33,8 +33,8 @@ def get_certificates_count( status='downloadable', course_id__in=get_base_queryset_courses( course_org_filter_list, - only_visible=only_visible_courses, - only_active=only_active_courses, + visible_filter=visible_courses_filter, + active_filter=active_courses_filter, ), ).annotate(course_org=Subquery( CourseOverview.objects.filter( diff --git a/futurex_openedx_extensions/dashboard/statistics/courses.py b/futurex_openedx_extensions/dashboard/statistics/courses.py index 59bfb205..4d12b538 100644 --- a/futurex_openedx_extensions/dashboard/statistics/courses.py +++ b/futurex_openedx_extensions/dashboard/statistics/courses.py @@ -12,22 +12,24 @@ from futurex_openedx_extensions.helpers.tenants import get_course_org_filter_list -def get_courses_count(tenant_ids: List[int], only_visible: bool = True, only_active: bool = False) -> QuerySet: +def get_courses_count(tenant_ids: List[int], visible_filter: bool = True, active_filter: bool = None) -> QuerySet: """ Get the count of courses in the given tenants :param tenant_ids: List of tenant IDs to get the count for :type tenant_ids: List[int] - :param only_visible: Whether to only count courses that are visible in the catalog - :type only_visible: bool - :param only_active: Whether to only count active courses (according to dates) - :type only_active: bool + :param visible_filter: Value to filter courses on catalog visibility. None means no filter. + :type visible_filter: bool + :param active_filter: Value to filter courses on active status. None means no filter. + :type active_filter: bool :return: QuerySet of courses count per organization :rtype: QuerySet """ course_org_filter_list = get_course_org_filter_list(tenant_ids)['course_org_filter_list'] - q_set = get_base_queryset_courses(course_org_filter_list, only_visible=only_visible, only_active=only_active) + q_set = get_base_queryset_courses( + course_org_filter_list, visible_filter=visible_filter, active_filter=active_filter + ) return q_set.values('org').annotate( courses_count=Count('id') @@ -35,23 +37,25 @@ def get_courses_count(tenant_ids: List[int], only_visible: bool = True, only_act def get_courses_count_by_status( - tenant_ids: List[int], only_visible: bool = True, only_active: bool = False + tenant_ids: List[int], visible_filter: bool = True, active_filter: bool = None ) -> QuerySet: """ Get the count of courses in the given tenants by status :param tenant_ids: List of tenant IDs to get the count for :type tenant_ids: List[int] - :param only_visible: Whether to only count courses that are visible in the catalog - :type only_visible: bool - :param only_active: Whether to only count active courses (according to dates) - :type only_active: bool + :param visible_filter: Whether to only count courses that are visible in the catalog + :type visible_filter: bool + :param active_filter: Whether to only count active courses (according to dates) + :type active_filter: bool :return: QuerySet of courses count per organization and status :rtype: QuerySet """ course_org_filter_list = get_course_org_filter_list(tenant_ids)['course_org_filter_list'] - q_set = get_base_queryset_courses(course_org_filter_list, only_visible=only_visible, only_active=only_active) + q_set = get_base_queryset_courses( + course_org_filter_list, visible_filter=visible_filter, active_filter=active_filter + ) q_set = q_set.annotate( status=Case( diff --git a/futurex_openedx_extensions/dashboard/statistics/learners.py b/futurex_openedx_extensions/dashboard/statistics/learners.py index f06a0fb9..80f555b4 100644 --- a/futurex_openedx_extensions/dashboard/statistics/learners.py +++ b/futurex_openedx_extensions/dashboard/statistics/learners.py @@ -13,7 +13,7 @@ def get_learners_count_having_enrollment_per_org( - tenant_id: int, only_visible_courses: bool = True, only_active_courses: bool = False + tenant_id: int, visible_courses_filter: bool = True, active_courses_filter: bool = None ) -> QuerySet: """ TODO: Cache the result of this function @@ -22,17 +22,17 @@ def get_learners_count_having_enrollment_per_org( :param tenant_id: Tenant ID to get the count for :type tenant_id: int - :param only_visible_courses: Whether to only count courses that are visible in the catalog - :type only_visible_courses: bool - :param only_active_courses: Whether to only count active courses (according to dates) - :type only_active_courses: bool + :param visible_courses_filter: Value to filter courses on catalog visibility. None means no filter. + :type visible_courses_filter: bool + :param active_courses_filter: Value to filter courses on active status. None means no filter. + :type active_courses_filter: bool :return: QuerySet of learners count per organization :rtype: QuerySet """ course_org_filter_list = get_course_org_filter_list([tenant_id])['course_org_filter_list'] queryset = get_base_queryset_courses( - course_org_filter_list, only_visible=only_visible_courses, only_active=only_active_courses, + course_org_filter_list, visible_filter=visible_courses_filter, active_filter=active_courses_filter, ) return queryset.values('org').annotate( @@ -53,7 +53,7 @@ def get_learners_count_having_enrollment_per_org( def get_learners_count_having_enrollment_for_tenant( - tenant_id: int, only_visible_courses: bool = True, only_active_courses: bool = False + tenant_id: int, visible_courses_filter: bool = True, active_courses_filter: bool = None ) -> QuerySet: """ TODO: Cache the result of this function @@ -61,10 +61,10 @@ def get_learners_count_having_enrollment_for_tenant( :param tenant_id: Tenant ID to get the count for :type tenant_id: int - :param only_visible_courses: Whether to only count courses that are visible in the catalog - :type only_visible_courses: bool - :param only_active_courses: Whether to only count active courses (according to dates) - :type only_active_courses: bool + :param visible_courses_filter: Value to filter courses on catalog visibility. None means no filter. + :type visible_courses_filter: bool + :param active_courses_filter: Value to filter courses on active status. None means no filter. + :type active_courses_filter: bool :return: QuerySet of learners count per organization :rtype: QuerySet """ @@ -76,8 +76,8 @@ def get_learners_count_having_enrollment_for_tenant( is_active=True, courseenrollment__course_id__in=get_base_queryset_courses( course_org_filter_list, - only_visible=only_visible_courses, - only_active=only_active_courses, + visible_filter=visible_courses_filter, + active_filter=active_courses_filter, ), ).exclude( Exists( @@ -90,7 +90,7 @@ def get_learners_count_having_enrollment_for_tenant( def get_learners_count_having_no_enrollment( - tenant_id: int, only_visible_courses: bool = True, only_active_courses: bool = False + tenant_id: int, visible_courses_filter: bool = True, active_courses_filter: bool = None ) -> QuerySet: """ TODO: Cache the result of this function @@ -101,10 +101,10 @@ def get_learners_count_having_no_enrollment( :param tenant_id: Tenant ID to get the count for :type tenant_id: int - :param only_visible_courses: Whether to only count courses that are visible in the catalog - :type only_visible_courses: bool - :param only_active_courses: Whether to only count active courses (according to dates) - :type only_active_courses: bool + :param visible_courses_filter: Value to filter courses on catalog visibility. None means no filter. + :type visible_courses_filter: bool + :param active_courses_filter: Value to filter courses on active status. None means no filter. + :type active_courses_filter: bool :return: QuerySet of learners count per organization :rtype: QuerySet """ @@ -119,8 +119,8 @@ def get_learners_count_having_no_enrollment( user_id=OuterRef('user_id'), course_id__in=get_base_queryset_courses( course_org_filter_list, - only_visible=only_visible_courses, - only_active=only_active_courses, + visible_filter=visible_courses_filter, + active_filter=active_courses_filter, ), user__is_superuser=False, user__is_staff=False, diff --git a/futurex_openedx_extensions/helpers/querysets.py b/futurex_openedx_extensions/helpers/querysets.py index 93e2718f..ba0bc508 100644 --- a/futurex_openedx_extensions/helpers/querysets.py +++ b/futurex_openedx_extensions/helpers/querysets.py @@ -3,7 +3,8 @@ from typing import List -from django.db.models import Q +from common.djangoapps.student.models import UserSignupSource +from django.db.models import BooleanField, Case, Exists, OuterRef, Q, Value, When from django.db.models.query import QuerySet from django.utils.timezone import now from openedx.core.djangoapps.content.course_overviews.models import CourseOverview @@ -11,29 +12,66 @@ def get_base_queryset_courses( course_org_filter_list: List[str], - only_visible: bool = True, - only_active: bool = False, + visible_filter: bool | None = True, + active_filter: bool | None = None, ) -> QuerySet: """ Get the default course queryset for the given filters. :param course_org_filter_list: List of course organizations to filter by :type course_org_filter_list: List[str] - :param only_visible: Whether to only include courses that are visible in the catalog - :type only_visible: bool - :param only_active: Whether to only include active courses - :type only_active: bool + :param visible_filter: Value to filter courses on catalog visibility. None means no filter. + :type visible_filter: bool + :param active_filter: Value to filter courses on active status. None means no filter. + :type active_filter: bool :return: QuerySet of courses :rtype: QuerySet """ - q_set = CourseOverview.objects.filter(org__in=course_org_filter_list) - if only_active: - q_set = q_set.filter( - Q(start__isnull=True) | Q(start__lte=now()), - ).filter( - Q(end__isnull=True) | Q(end__gte=now()), - ) - if only_visible: - q_set = q_set.filter(catalog_visibility__in=['about', 'both']) + now_time = now() + course_is_active_queryset = ( + (Q(start__isnull=True) | Q(start__lte=now_time)) & + (Q(end__isnull=True) | Q(end__gte=now_time)) + ) + + course_is_visible_queryset = Q(catalog_visibility__in=['about', 'both']) & Q(visible_to_staff_only=False) + + q_set = CourseOverview.objects.filter( + org__in=course_org_filter_list, + ).annotate( + course_is_active=Case( + When(course_is_active_queryset, then=Value(True)), + default=Value(False), + output_field=BooleanField(), + ), + ).annotate( + course_is_visible=Case( + When(course_is_visible_queryset, then=Value(True)), + default=Value(False), + output_field=BooleanField(), + ), + ) + + if active_filter is not None: + q_set = q_set.filter(course_is_active=active_filter) + + if visible_filter is not None: + q_set = q_set.filter(course_is_visible=visible_filter) return q_set + + +def get_has_site_login_queryset(tenant_sites: List[str]) -> QuerySet: + """ + Get the queryset of users who have logged in to any of the given tenant sites. + + :param tenant_sites: List of tenant sites to check for + :type tenant_sites: List[str] + :return: QuerySet of users + :rtype: QuerySet + """ + return Exists( + UserSignupSource.objects.filter( + user_id=OuterRef('id'), + site__in=tenant_sites + ) + ) diff --git a/futurex_openedx_extensions/helpers/tenants.py b/futurex_openedx_extensions/helpers/tenants.py index 7a458c0f..3c9ba138 100644 --- a/futurex_openedx_extensions/helpers/tenants.py +++ b/futurex_openedx_extensions/helpers/tenants.py @@ -254,3 +254,22 @@ def get_selected_tenants(request: Request) -> List[int]: if tenant_ids is None: return get_accessible_tenant_ids(request.user) return ids_string_to_list(tenant_ids) + + +def get_tenants_sites(tenant_ids: List[int]) -> List[str]: + """ + Get the sites for the given tenant IDs + + :param tenant_ids: List of tenant IDs + :type tenant_ids: List[int] + :return: List of sites + :rtype: List[str] + """ + if not tenant_ids: + return [] + + tenant_sites = [] + for tenant_id in tenant_ids: + if site := get_tenant_site(tenant_id): + tenant_sites.append(site) + return tenant_sites diff --git a/test_utils/edx_platform_mocks/fake_models/models.py b/test_utils/edx_platform_mocks/fake_models/models.py index 330f5569..0bd01ffb 100644 --- a/test_utils/edx_platform_mocks/fake_models/models.py +++ b/test_utils/edx_platform_mocks/fake_models/models.py @@ -16,6 +16,7 @@ class CourseOverview(models.Model): enrollment_end = models.DateTimeField(null=True) self_paced = models.BooleanField(default=False) course_image_url = models.TextField() + visible_to_staff_only = models.BooleanField(default=False) class Meta: app_label = "fake_models" diff --git a/tests/test_dashboard/test_details/test_details_learners.py b/tests/test_dashboard/test_details/test_details_learners.py index 90c287c6..e7da8b2c 100644 --- a/tests/test_dashboard/test_details/test_details_learners.py +++ b/tests/test_dashboard/test_details/test_details_learners.py @@ -1,17 +1,52 @@ """Tests for learner details collectors""" import pytest +from django.contrib.auth import get_user_model -from futurex_openedx_extensions.dashboard.details.learners import get_learners_queryset +from futurex_openedx_extensions.dashboard.details.learners import ( + get_certificates_count_for_learner_queryset, + get_courses_count_for_learner_queryset, + get_learners_queryset, +) @pytest.mark.django_db -@pytest.mark.parametrize('tenant_ids, search_text, expected_count', [ +@pytest.mark.parametrize("function_to_test, username, expected_count, assert_error_message", [ + ("courses", "user4", 0, "user4 should report zero courses in ORG1 and ORG2 because of being an org admin"), + ("certificates", "user4", 2, "user4 should report all certificates regardless of being an org admin"), + ("courses", "user3", 1, "user3 should report courses in ORG2 but not ORG2 because of course access role"), + ("certificates", "user3", 1, "user3 should report all certificates regardless of course access role"), + ("courses", "user5", 2, "user5 should report all courses in ORG1 and ORG2"), + ("certificates", "user5", 1, "user5 should report all certificates regardless of course access role"), +]) +def test_count_for_learner_queryset( + base_data, function_to_test, username, expected_count, assert_error_message +): # pylint: disable=unused-argument + """Verify that get_certificates_count_for_learner_queryset returns the correct QuerySet.""" + assert function_to_test in ["courses", "certificates"], f"bad test data (function_to_test = {function_to_test})" + + queryset = get_user_model().objects.filter(username=username) + assert queryset.count() == 1, f"bad test data (username = {username})" + + course_org_filter_list = ["ORG1", "ORG2"] + if function_to_test == "courses": + fnc = get_courses_count_for_learner_queryset + else: + fnc = get_certificates_count_for_learner_queryset + queryset = get_user_model().objects.filter(username=username).annotate( + result=fnc(course_org_filter_list) + ) + + assert queryset.all()[0].result == expected_count, f"{assert_error_message} +. Check the test data for details." + + +@pytest.mark.django_db +@pytest.mark.parametrize("tenant_ids, search_text, expected_count", [ ([7, 8], None, 22), ([7], None, 17), - ([7], 'user', 17), - ([7], 'user4', 10), - ([7], 'user5', 1), - ([7], 'user6', 0), + ([7], "user", 17), + ([7], "user4", 10), + ([7], "user5", 1), + ([7], "user6", 0), ([4], None, 0), ]) def test_get_learners_queryset(base_data, tenant_ids, search_text, expected_count): # pylint: disable=unused-argument diff --git a/tests/test_helpers/test_querysets.py b/tests/test_helpers/test_querysets.py index 26cdb08e..c984c093 100644 --- a/tests/test_helpers/test_querysets.py +++ b/tests/test_helpers/test_querysets.py @@ -1,35 +1,62 @@ """Tests for querysets helpers""" import pytest +from django.contrib.auth import get_user_model from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from futurex_openedx_extensions.helpers.querysets import get_base_queryset_courses +from futurex_openedx_extensions.helpers import querysets @pytest.mark.django_db def test_get_base_queryset_courses(base_data): # pylint: disable=unused-argument """Verify get_base_queryset_courses function.""" - result = get_base_queryset_courses(["ORG1", "ORG2"]) + result = querysets.get_base_queryset_courses(["ORG1", "ORG2"]) assert result.count() == 12 for course in result: assert course.catalog_visibility == "both" @pytest.mark.django_db -def test_get_base_queryset_courses_not_only_visible(base_data): # pylint: disable=unused-argument - """Verify get_base_queryset_courses function with only_visible=False.""" +def test_get_base_queryset_courses_visible_filter(base_data): # pylint: disable=unused-argument + """Verify get_base_queryset_courses function with visible_filter.""" course = CourseOverview.objects.filter(org="ORG1").first() assert course.catalog_visibility == "both", "Catalog visibility should be initialized as (both) for test courses" course.catalog_visibility = "none" course.save() - result = get_base_queryset_courses(["ORG1", "ORG2"]) + result = querysets.get_base_queryset_courses(["ORG1", "ORG2"]) assert result.count() == 11 - result = get_base_queryset_courses(["ORG1", "ORG2"], only_visible=False) + result = querysets.get_base_queryset_courses(["ORG1", "ORG2"], visible_filter=False) + assert result.count() == 1 + result = querysets.get_base_queryset_courses(["ORG1", "ORG2"], visible_filter=None) assert result.count() == 12 @pytest.mark.django_db -def test_get_base_queryset_courses_only_active(base_data): # pylint: disable=unused-argument - """Verify get_base_queryset_courses function with only_active=True.""" - result = get_base_queryset_courses(["ORG1", "ORG2"], only_active=True) +def test_get_base_queryset_courses_active_filter(base_data): # pylint: disable=unused-argument + """Verify get_base_queryset_courses function with active_filter.""" + result = querysets.get_base_queryset_courses(["ORG1", "ORG2"]) + assert result.count() == 12 + result = querysets.get_base_queryset_courses(["ORG1", "ORG2"], active_filter=True) assert result.count() == 7 + result = querysets.get_base_queryset_courses(["ORG1", "ORG2"], active_filter=False) + assert result.count() == 5 + + +@pytest.mark.django_db +@pytest.mark.parametrize("sites, expected", [ + (["s1.sample.com"], True), + (["s2.sample.com"], True), + (["s3.sample.com"], False), + (["s1.sample.com", "s2.sample.com"], True), + (["s1.sample.com", "s3.sample.com"], True), + (["s2.sample.com", "s3.sample.com"], True), +]) +def test_get_has_site_login_queryset(base_data, sites, expected): # pylint: disable=unused-argument + """Verify get_has_site_login_queryset function.""" + result = get_user_model().objects.filter( + username="user4", + ).annotate( + has_site_login=querysets.get_has_site_login_queryset(sites), + ) + assert result.count() == 1 + assert result.first().has_site_login == expected diff --git a/tests/test_helpers/test_tenants.py b/tests/test_helpers/test_tenants.py index 7999acfb..48d689a8 100644 --- a/tests/test_helpers/test_tenants.py +++ b/tests/test_helpers/test_tenants.py @@ -261,3 +261,27 @@ def test_get_tenant_site(base_data, tenant_id, expected): # pylint: disable=unu def test_get_tenants_by_org(base_data, org, expected): # pylint: disable=unused-argument """Verify get_tenants_by_org function.""" assert expected == tenants.get_tenants_by_org(org) + + +@pytest.mark.django_db +@pytest.mark.parametrize("tenant_ids, expected", [ + ([1], ['s1.sample.com']), + ([2, 3], ['s2.sample.com', 's3.sample.com']), + ([2, 3, 4], ['s2.sample.com', 's3.sample.com']), + ([2, 3, 7, 8], ['s2.sample.com', 's3.sample.com', 's7.sample.com', 's8.sample.com']), +]) +def test_get_tenants_sites(base_data, tenant_ids, expected): # pylint: disable=unused-argument + """Verify get_tenants_sites function.""" + assert expected == tenants.get_tenants_sites(tenant_ids) + + +@pytest.mark.django_db +@pytest.mark.parametrize("tenant_ids", [ + [], + None, + [99], +]) +def test_get_tenants_sites_bad_tenants(base_data, tenant_ids): # pylint: disable=unused-argument + """Verify get_tenants_sites function.""" + result = tenants.get_tenants_sites(tenant_ids) + assert result is not None and len(result) == 0