test_tabs.py 36.1 KB
Newer Older
1 2 3
"""
Test cases for tabs.
"""
4

5
import pytest
6 7
from django.core.urlresolvers import reverse
from django.http import Http404
8
from milestones.tests.utils import MilestonesTestCaseMixin
9
from mock import MagicMock, Mock, patch
10
from nose.plugins.attrib import attr
11

12
from courseware.courses import get_course_by_id
13
from courseware.tabs import (
14 15 16 17 18 19
    CourseInfoTab,
    CoursewareTab,
    ExternalDiscussionCourseTab,
    ExternalLinkCourseTab,
    ProgressTab,
    get_course_tab_list
20
)
21
from courseware.tests.factories import InstructorFactory, StaffFactory
22 23
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.views.views import StaticCourseTabView, get_static_tab_fragment
24
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
25
from openedx.core.djangolib.testing.utils import get_mock_request
26
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
27
from student.models import CourseEnrollment
28
from student.tests.factories import UserFactory
29
from util.milestones_helpers import (
30
    add_course_content_milestone,
31
    add_course_milestone,
32 33
    add_milestone,
    get_milestone_relationship_types
34
)
35 36
from xmodule import tabs as xmodule_tabs
from xmodule.modulestore.tests.django_utils import (
37
    TEST_DATA_MIXED_MODULESTORE,
38
    ModuleStoreTestCase,
39
    SharedModuleStoreTestCase
40
)
41
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
42 43
from xmodule.modulestore.tests.utils import TEST_DATA_DIR
from xmodule.modulestore.xml_importer import import_course_from_xml
44

45

46
class TabTestCase(SharedModuleStoreTestCase):
47
    """Base class for Tab-related test cases."""
48 49 50 51 52 53 54 55
    @classmethod
    def setUpClass(cls):
        super(TabTestCase, cls).setUpClass()

        cls.course = CourseFactory.create(org='edX', course='toy', run='2012_Fall')
        cls.fake_dict_tab = {'fake_key': 'fake_value'}
        cls.books = None

56 57 58 59 60 61 62 63
    def setUp(self):
        super(TabTestCase, self).setUp()
        self.reverse = lambda name, args: "name/{0}/args/{1}".format(name, ",".join(str(a) for a in args))

    def create_mock_user(self, is_authenticated=True, is_staff=True, is_enrolled=True):
        """
        Creates a mock user with the specified properties.
        """
64
        user = UserFactory(is_staff=is_staff)
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
        user.is_enrolled = is_enrolled
        user.is_authenticated = lambda: is_authenticated
        return user

    def is_tab_enabled(self, tab, course, user):
        """
        Returns true if the specified tab is enabled.
        """
        return tab.is_enabled(course, user=user)

    def set_up_books(self, num_books):
        """Initializes the textbooks in the course and adds the given number of books to each textbook"""
        self.books = [MagicMock() for _ in range(num_books)]
        for book_index, book in enumerate(self.books):
            book.title = 'Book{0}'.format(book_index)
        self.course.textbooks = self.books
        self.course.pdf_textbooks = self.books
        self.course.html_textbooks = self.books

    def check_tab(
            self,
            tab_class,
            dict_tab,
            expected_link,
            expected_tab_id,
            expected_name='same',
            invalid_dict_tab=None,
    ):
        """
        Helper method to verify a tab class.

        'tab_class' is the class of the tab that is being tested
        'dict_tab' is the raw dictionary value of the tab
        'expected_link' is the expected value for the hyperlink of the tab
        'expected_tab_id' is the expected value for the unique id of the tab
        'expected_name' is the expected value for the name of the tab
        'invalid_dict_tab' is an invalid dictionary value for the tab.
            Can be 'None' if the given tab class does not have any keys to validate.
        """
        # create tab
105
        tab = tab_class(tab_dict=dict_tab)
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231

        # name is as expected
        self.assertEqual(tab.name, expected_name)

        # link is as expected
        self.assertEqual(tab.link_func(self.course, self.reverse), expected_link)

        # verify active page name
        self.assertEqual(tab.tab_id, expected_tab_id)

        # validate tab
        self.assertTrue(tab.validate(dict_tab))
        if invalid_dict_tab:
            with self.assertRaises(xmodule_tabs.InvalidTabsException):
                tab.validate(invalid_dict_tab)

        # check get and set methods
        self.check_get_and_set_methods(tab)

        # check to_json and from_json methods
        self.check_tab_json_methods(tab)

        # check equality methods
        self.check_tab_equality(tab, dict_tab)

        # return tab for any additional tests
        return tab

    def check_tab_equality(self, tab, dict_tab):
        """Tests the equality methods on the given tab"""
        self.assertEquals(tab, dict_tab)  # test __eq__
        ne_dict_tab = dict_tab
        ne_dict_tab['type'] = 'fake_type'
        self.assertNotEquals(tab, ne_dict_tab)  # test __ne__: incorrect type
        self.assertNotEquals(tab, {'fake_key': 'fake_value'})  # test __ne__: missing type

    def check_tab_json_methods(self, tab):
        """Tests the json from and to methods on the given tab"""
        serialized_tab = tab.to_json()
        deserialized_tab = tab.from_json(serialized_tab)
        self.assertEquals(serialized_tab, deserialized_tab)

    def check_can_display_results(
            self,
            tab,
            expected_value=True,
            for_authenticated_users_only=False,
            for_staff_only=False,
            for_enrolled_users_only=False
    ):
        """Checks can display results for various users"""
        if for_staff_only:
            user = self.create_mock_user(is_authenticated=True, is_staff=True, is_enrolled=True)
            self.assertEquals(expected_value, self.is_tab_enabled(tab, self.course, user))
        if for_authenticated_users_only:
            user = self.create_mock_user(is_authenticated=True, is_staff=False, is_enrolled=False)
            self.assertEquals(expected_value, self.is_tab_enabled(tab, self.course, user))
        if not for_staff_only and not for_authenticated_users_only and not for_enrolled_users_only:
            user = self.create_mock_user(is_authenticated=False, is_staff=False, is_enrolled=False)
            self.assertEquals(expected_value, self.is_tab_enabled(tab, self.course, user))
        if for_enrolled_users_only:
            user = self.create_mock_user(is_authenticated=True, is_staff=False, is_enrolled=True)
            self.assertEquals(expected_value, self.is_tab_enabled(tab, self.course, user))

    def check_get_and_set_methods(self, tab):
        """Test __getitem__ and __setitem__ calls"""
        self.assertEquals(tab['type'], tab.type)
        self.assertEquals(tab['tab_id'], tab.tab_id)
        with self.assertRaises(KeyError):
            _ = tab['invalid_key']

        self.check_get_and_set_method_for_key(tab, 'name')
        self.check_get_and_set_method_for_key(tab, 'tab_id')
        with self.assertRaises(KeyError):
            tab['invalid_key'] = 'New Value'

    def check_get_and_set_method_for_key(self, tab, key):
        """Test __getitem__ and __setitem__ for the given key"""
        old_value = tab[key]
        new_value = 'New Value'
        tab[key] = new_value
        self.assertEquals(tab[key], new_value)
        tab[key] = old_value
        self.assertEquals(tab[key], old_value)


class TextbooksTestCase(TabTestCase):
    """Test cases for Textbook Tab."""

    def setUp(self):
        super(TextbooksTestCase, self).setUp()

        self.set_up_books(2)

        self.dict_tab = MagicMock()
        self.course.tabs = [
            xmodule_tabs.CourseTab.load('textbooks'),
            xmodule_tabs.CourseTab.load('pdf_textbooks'),
            xmodule_tabs.CourseTab.load('html_textbooks'),
        ]
        self.num_textbook_tabs = sum(1 for tab in self.course.tabs if tab.type in [
            'textbooks', 'pdf_textbooks', 'html_textbooks'
        ])
        self.num_textbooks = self.num_textbook_tabs * len(self.books)

    @patch.dict("django.conf.settings.FEATURES", {"ENABLE_TEXTBOOK": True})
    def test_textbooks_enabled(self):

        type_to_reverse_name = {'textbook': 'book', 'pdftextbook': 'pdf_book', 'htmltextbook': 'html_book'}

        num_textbooks_found = 0
        user = self.create_mock_user(is_authenticated=True, is_staff=False, is_enrolled=True)
        for tab in xmodule_tabs.CourseTabList.iterate_displayable(self.course, user=user):
            # verify all textbook type tabs
            if tab.type == 'single_textbook':
                book_type, book_index = tab.tab_id.split("/", 1)
                expected_link = self.reverse(
                    type_to_reverse_name[book_type],
                    args=[self.course.id.to_deprecated_string(), book_index]
                )
                self.assertEqual(tab.link_func(self.course, self.reverse), expected_link)
                self.assertTrue(tab.name.startswith('Book{0}'.format(book_index)))
                num_textbooks_found = num_textbooks_found + 1
        self.assertEquals(num_textbooks_found, self.num_textbooks)


232
@attr(shard=1)
233
class StaticTabDateTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
234 235
    """Test cases for Static Tab Dates."""

236
    MODULESTORE = TEST_DATA_MIXED_MODULESTORE
237

238 239 240 241 242 243
    @classmethod
    def setUpClass(cls):
        super(StaticTabDateTestCase, cls).setUpClass()
        cls.course = CourseFactory.create()
        cls.page = ItemFactory.create(
            category="static_tab", parent_location=cls.course.location,
244 245
            data="OOGIE BLOOGIE", display_name="new_tab"
        )
246 247 248
        cls.course.tabs.append(xmodule_tabs.CourseTab.load('static_tab', name='New Tab', url_slug='new_tab'))
        cls.course.save()

249 250
    def test_logged_in(self):
        self.setup_user()
251
        url = reverse('static_tab', args=[self.course.id.to_deprecated_string(), 'new_tab'])
252 253 254 255 256
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn("OOGIE BLOOGIE", resp.content)

    def test_anonymous_user(self):
257
        url = reverse('static_tab', args=[self.course.id.to_deprecated_string(), 'new_tab'])
258 259 260 261
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn("OOGIE BLOOGIE", resp.content)

262
    def test_invalid_course_key(self):
263
        self.setup_user()
264
        request = get_mock_request(self.user)
265
        with self.assertRaises(Http404):
266
            StaticCourseTabView().get(request, course_id='edX/toy', tab_slug='new_tab')
267

268
    def test_get_static_tab_fragment(self):
269
        self.setup_user()
John Eskew committed
270
        course = get_course_by_id(self.course.id)
271
        request = get_mock_request(self.user)
John Eskew committed
272
        tab = xmodule_tabs.CourseTabList.get_tab_by_slug(course.tabs, 'new_tab')
273 274

        # Test render works okay
275
        tab_content = get_static_tab_fragment(request, course, tab).content
John Eskew committed
276
        self.assertIn(self.course.id.to_deprecated_string(), tab_content)
277 278 279
        self.assertIn('static_tab', tab_content)

        # Test when render raises an exception
280
        with patch('courseware.views.views.get_module') as mock_module_render:
281 282 283
            mock_module_render.return_value = MagicMock(
                render=Mock(side_effect=Exception('Render failed!'))
            )
284 285
            static_tab_content = get_static_tab_fragment(request, course, tab).content
            self.assertIn("this module is temporarily unavailable", static_tab_content)
286

Adam Palay committed
287

288
@attr(shard=1)
Adam Palay committed
289
class StaticTabDateTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
290 291 292
    """
    Tests for the static tab dates of an XML course
    """
293

294
    MODULESTORE = TEST_DATA_MIXED_MODULESTORE
295

296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
    def setUp(self):
        """
        Set up the tests
        """
        super(StaticTabDateTestCaseXML, self).setUp()

        # The following XML test course (which lives at common/test/data/2014)
        # is closed; we're testing that tabs still appear when
        # the course is already closed
        self.xml_course_key = self.store.make_course_key('edX', 'detached_pages', '2014')
        import_course_from_xml(
            self.store,
            'test_user',
            TEST_DATA_DIR,
            source_dirs=['2014'],
            static_content_store=None,
            target_id=self.xml_course_key,
            raise_on_failure=True,
            create_if_not_present=True,
        )
Adam Palay committed
316

317 318 319 320
        # this text appears in the test course's tab
        # common/test/data/2014/tabs/8e4cce2b4aaf4ba28b1220804619e41f.html
        self.xml_data = "static 463139"
        self.xml_url = "8e4cce2b4aaf4ba28b1220804619e41f"
Adam Palay committed
321

322 323 324
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
    def test_logged_in_xml(self):
        self.setup_user()
325
        url = reverse('static_tab', args=[self.xml_course_key.to_deprecated_string(), self.xml_url])
326 327 328 329 330 331
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn(self.xml_data, resp.content)

    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
    def test_anonymous_user_xml(self):
332
        url = reverse('static_tab', args=[self.xml_course_key.to_deprecated_string(), self.xml_url])
333 334 335
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn(self.xml_data, resp.content)
336 337


338
@attr(shard=1)
339
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
340
class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTestCaseMixin):
341 342 343
    """
    Validate tab behavior when dealing with Entrance Exams
    """
344
    MODULESTORE = TEST_DATA_MIXED_MODULESTORE
345

346
    @patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
347 348 349 350 351
    def setUp(self):
        """
        Test case scaffolding
        """
        super(EntranceExamsTabsTestCase, self).setUp()
352

353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
        self.course = CourseFactory.create()
        self.instructor_tab = ItemFactory.create(
            category="instructor", parent_location=self.course.location,
            data="Instructor Tab", display_name="Instructor"
        )
        self.extra_tab_2 = ItemFactory.create(
            category="static_tab", parent_location=self.course.location,
            data="Extra Tab", display_name="Extra Tab 2"
        )
        self.extra_tab_3 = ItemFactory.create(
            category="static_tab", parent_location=self.course.location,
            data="Extra Tab", display_name="Extra Tab 3"
        )
        self.setup_user()
        self.enroll(self.course)
        self.user.is_staff = True
        self.relationship_types = get_milestone_relationship_types()
370

371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
    def test_get_course_tabs_list_entrance_exam_enabled(self):
        """
        Unit Test: test_get_course_tabs_list_entrance_exam_enabled
        """
        entrance_exam = ItemFactory.create(
            category="chapter",
            parent_location=self.course.location,
            data="Exam Data",
            display_name="Entrance Exam",
            is_entrance_exam=True
        )
        milestone = {
            'name': 'Test Milestone',
            'namespace': '{}.entrance_exams'.format(unicode(self.course.id)),
            'description': 'Testing Courseware Tabs'
        }
        self.user.is_staff = False
388
        request = get_mock_request(self.user)
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
        self.course.entrance_exam_enabled = True
        self.course.entrance_exam_id = unicode(entrance_exam.location)
        milestone = add_milestone(milestone)
        add_course_milestone(
            unicode(self.course.id),
            self.relationship_types['REQUIRES'],
            milestone
        )
        add_course_content_milestone(
            unicode(self.course.id),
            unicode(entrance_exam.location),
            self.relationship_types['FULFILLS'],
            milestone
        )
        course_tab_list = get_course_tab_list(request, self.course)
        self.assertEqual(len(course_tab_list), 1)
        self.assertEqual(course_tab_list[0]['tab_id'], 'courseware')
        self.assertEqual(course_tab_list[0]['name'], 'Entrance Exam')

    def test_get_course_tabs_list_skipped_entrance_exam(self):
        """
        Tests tab list is not limited if user is allowed to skip entrance exam.
        """
        #create a user
        student = UserFactory()
        # login as instructor hit skip entrance exam api in instructor app
        instructor = InstructorFactory(course_key=self.course.id)
        self.client.logout()
        self.client.login(username=instructor.username, password='test')

        url = reverse('mark_student_can_skip_entrance_exam', kwargs={'course_id': unicode(self.course.id)})
        response = self.client.post(url, {
            'unique_student_identifier': student.email,
        })
        self.assertEqual(response.status_code, 200)

        # log in again as student
        self.client.logout()
        self.login(self.email, self.password)
428
        request = get_mock_request(self.user)
429 430 431 432 433 434 435 436 437 438 439 440
        course_tab_list = get_course_tab_list(request, self.course)
        self.assertEqual(len(course_tab_list), 5)

    def test_course_tabs_list_for_staff_members(self):
        """
        Tests tab list is not limited if user is member of staff
        and has not passed entrance exam.
        """
        # Login as member of staff
        self.client.logout()
        staff_user = StaffFactory(course_key=self.course.id)
        self.client.login(username=staff_user.username, password='test')
441
        request = get_mock_request(staff_user)
442 443
        course_tab_list = get_course_tab_list(request, self.course)
        self.assertEqual(len(course_tab_list), 5)
444

445

446
@attr(shard=1)
447
class TextBookCourseViewsTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
448 449 450
    """
    Validate tab behavior when dealing with textbooks.
    """
451
    MODULESTORE = TEST_DATA_MIXED_MODULESTORE
452

453 454 455 456 457
    @classmethod
    def setUpClass(cls):
        super(TextBookCourseViewsTestCase, cls).setUpClass()
        cls.course = CourseFactory.create()

458
    def setUp(self):
459
        super(TextBookCourseViewsTestCase, self).setUp()
460

461 462 463
        self.set_up_books(2)
        self.setup_user()
        self.enroll(self.course)
464 465 466
        self.num_textbook_tabs = sum(1 for tab in self.course.tabs if tab.type in [
            'textbooks', 'pdf_textbooks', 'html_textbooks'
        ])
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
        self.num_textbooks = self.num_textbook_tabs * len(self.books)

    def set_up_books(self, num_books):
        """Initializes the textbooks in the course and adds the given number of books to each textbook"""
        self.books = [MagicMock() for _ in range(num_books)]
        for book_index, book in enumerate(self.books):
            book.title = 'Book{0}'.format(book_index)
        self.course.textbooks = self.books
        self.course.pdf_textbooks = self.books
        self.course.html_textbooks = self.books

    def test_pdf_textbook_tabs(self):
        """
        Test that all textbooks tab links generating correctly.
        """
        type_to_reverse_name = {'textbook': 'book', 'pdftextbook': 'pdf_book', 'htmltextbook': 'html_book'}
483
        request = get_mock_request(self.user)
484
        course_tab_list = get_course_tab_list(request, self.course)
485 486 487
        num_of_textbooks_found = 0
        for tab in course_tab_list:
            # Verify links of all textbook type tabs.
488
            if tab.type == 'single_textbook':
489 490 491 492 493 494 495 496 497
                book_type, book_index = tab.tab_id.split("/", 1)
                expected_link = reverse(
                    type_to_reverse_name[book_type],
                    args=[self.course.id.to_deprecated_string(), book_index]
                )
                tab_link = tab.link_func(self.course, reverse)
                self.assertEqual(tab_link, expected_link)
                num_of_textbooks_found += 1
        self.assertEqual(num_of_textbooks_found, self.num_textbooks)
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513

    @patch.dict("django.conf.settings.FEATURES", {"ENABLE_TEXTBOOK": False})
    def test_textbooks_disabled(self):
        tab = xmodule_tabs.CourseTab.load('textbooks')
        self.assertFalse(tab.is_enabled(self.course, self.user))


class TabListTestCase(TabTestCase):
    """Base class for Test cases involving tab lists."""

    def setUp(self):
        super(TabListTestCase, self).setUp()

        # invalid tabs
        self.invalid_tabs = [
            # less than 2 tabs
514
            [{'type': CoursewareTab.type}],
515
            # missing course_info
516
            [{'type': CoursewareTab.type}, {'type': 'discussion', 'name': 'fake_name'}],
517
            [{'type': 'unknown_type'}],
518
            # incorrect order
519 520
            [{'type': 'discussion', 'name': 'fake_name'},
             {'type': CourseInfoTab.type, 'name': 'fake_name'}, {'type': CoursewareTab.type}],
521 522 523 524
        ]

        # tab types that should appear only once
        unique_tab_types = [
525 526
            CoursewareTab.type,
            CourseInfoTab.type,
527 528 529 530 531 532 533
            'textbooks',
            'pdf_textbooks',
            'html_textbooks',
        ]

        for unique_tab_type in unique_tab_types:
            self.invalid_tabs.append([
534 535
                {'type': CoursewareTab.type},
                {'type': CourseInfoTab.type, 'name': 'fake_name'},
536 537 538 539 540 541 542
                # add the unique tab multiple times
                {'type': unique_tab_type},
                {'type': unique_tab_type},
            ])

        # valid tabs
        self.valid_tabs = [
543 544
            # any empty list is valid because a default list of tabs will be
            # generated to replace the empty list.
545 546 547
            [],
            # all valid tabs
            [
548 549
                {'type': CoursewareTab.type},
                {'type': CourseInfoTab.type, 'name': 'fake_name'},
550
                {'type': 'discussion', 'name': 'fake_name'},
551
                {'type': ExternalLinkCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'},
552
                {'type': ExternalLinkCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'},
553 554 555
                {'type': 'textbooks'},
                {'type': 'pdf_textbooks'},
                {'type': 'html_textbooks'},
556 557
                {'type': ProgressTab.type, 'name': 'fake_name'},
                {'type': xmodule_tabs.StaticTab.type, 'name': 'fake_name', 'url_slug': 'schlug'},
558 559 560 561
                {'type': 'syllabus'},
            ],
            # with external discussion
            [
562 563 564
                {'type': CoursewareTab.type},
                {'type': CourseInfoTab.type, 'name': 'fake_name'},
                {'type': ExternalDiscussionCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'}
565 566 567 568 569 570
            ],
        ]

        self.all_valid_tab_list = xmodule_tabs.CourseTabList().from_json(self.valid_tabs[1])


571
@attr(shard=1)
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592
class ValidateTabsTestCase(TabListTestCase):
    """Test cases for validating tabs."""

    def test_validate_tabs(self):
        tab_list = xmodule_tabs.CourseTabList()
        for invalid_tab_list in self.invalid_tabs:
            with self.assertRaises(xmodule_tabs.InvalidTabsException):
                tab_list.from_json(invalid_tab_list)

        for valid_tab_list in self.valid_tabs:
            from_json_result = tab_list.from_json(valid_tab_list)
            self.assertEquals(len(from_json_result), len(valid_tab_list))

    def test_invalid_tab_type(self):
        """
        Verifies that having an unrecognized tab type does not cause
        the tabs to be undisplayable.
        """
        tab_list = xmodule_tabs.CourseTabList()
        self.assertEquals(
            len(tab_list.from_json([
593 594
                {'type': CoursewareTab.type},
                {'type': CourseInfoTab.type, 'name': 'fake_name'},
595 596 597 598 599 600
                {'type': 'no_such_type'}
            ])),
            2
        )


601
@attr(shard=1)
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
class CourseTabListTestCase(TabListTestCase):
    """Testing the generator method for iterating through displayable tabs"""

    def has_tab(self, tab_list, tab_type):
        """ Searches the given lab_list for a given tab_type. """
        for tab in tab_list:
            if tab.type == tab_type:
                return True
        return False

    def test_initialize_default_without_syllabus(self):
        self.course.tabs = []
        self.course.syllabus_present = False
        xmodule_tabs.CourseTabList.initialize_default(self.course)
        self.assertFalse(self.has_tab(self.course.tabs, 'syllabus'))

    def test_initialize_default_with_syllabus(self):
        self.course.tabs = []
        self.course.syllabus_present = True
        xmodule_tabs.CourseTabList.initialize_default(self.course)
        self.assertTrue(self.has_tab(self.course.tabs, 'syllabus'))

    def test_initialize_default_with_external_link(self):
        self.course.tabs = []
        self.course.discussion_link = "other_discussion_link"
        xmodule_tabs.CourseTabList.initialize_default(self.course)
        self.assertTrue(self.has_tab(self.course.tabs, 'external_discussion'))
        self.assertFalse(self.has_tab(self.course.tabs, 'discussion'))

    def test_initialize_default_without_external_link(self):
        self.course.tabs = []
        self.course.discussion_link = ""
        xmodule_tabs.CourseTabList.initialize_default(self.course)
        self.assertFalse(self.has_tab(self.course.tabs, 'external_discussion'))
        self.assertTrue(self.has_tab(self.course.tabs, 'discussion'))

    @patch.dict("django.conf.settings.FEATURES", {
        "ENABLE_TEXTBOOK": True,
        "ENABLE_DISCUSSION_SERVICE": True,
        "ENABLE_STUDENT_NOTES": True,
        "ENABLE_EDXNOTES": True,
    })
    def test_iterate_displayable(self):
        self.course.hide_progress_tab = False

        # create 1 book per textbook type
        self.set_up_books(1)

        # initialize the course tabs to a list of all valid tabs
        self.course.tabs = self.all_valid_tab_list

        # enumerate the tabs with no user
654 655 656 657
        expected = [tab.type for tab in
                    xmodule_tabs.CourseTabList.iterate_displayable(self.course, inline_collections=False)]
        actual = [tab.type for tab in self.course.tabs if tab.is_enabled(self.course, user=None)]
        assert actual == expected
658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693

        # enumerate the tabs with a staff user
        user = UserFactory(is_staff=True)
        CourseEnrollment.enroll(user, self.course.id)
        for i, tab in enumerate(xmodule_tabs.CourseTabList.iterate_displayable(self.course, user=user)):
            if getattr(tab, 'is_collection_item', False):
                # a collection item was found as a result of a collection tab
                self.assertTrue(getattr(self.course.tabs[i], 'is_collection', False))
            else:
                # all other tabs must match the expected type
                self.assertEquals(tab.type, self.course.tabs[i].type)

        # test including non-empty collections
        self.assertIn(
            {'type': 'html_textbooks'},
            list(xmodule_tabs.CourseTabList.iterate_displayable(self.course, inline_collections=False)),
        )

        # test not including empty collections
        self.course.html_textbooks = []
        self.assertNotIn(
            {'type': 'html_textbooks'},
            list(xmodule_tabs.CourseTabList.iterate_displayable(self.course, inline_collections=False)),
        )

    def test_get_tab_by_methods(self):
        """Tests the get_tab methods in CourseTabList"""
        self.course.tabs = self.all_valid_tab_list
        for tab in self.course.tabs:

            # get tab by type
            self.assertEquals(xmodule_tabs.CourseTabList.get_tab_by_type(self.course.tabs, tab.type), tab)

            # get tab by id
            self.assertEquals(xmodule_tabs.CourseTabList.get_tab_by_id(self.course.tabs, tab.tab_id), tab)

694
    @pytest.mark.django111_expected_failure
695 696 697 698 699 700 701 702 703 704 705 706 707
    def test_course_tabs_staff_only(self):
        """
        Tests the static tabs that available only for instructor
        """
        self.course.tabs.append(xmodule_tabs.CourseTab.load('static_tab', name='Static Tab Free',
                                                            url_slug='extra_tab_1',
                                                            course_staff_only=False))
        self.course.tabs.append(xmodule_tabs.CourseTab.load('static_tab', name='Static Tab Instructors Only',
                                                            url_slug='extra_tab_2',
                                                            course_staff_only=True))
        self.course.save()

        user = self.create_mock_user(is_authenticated=True, is_staff=False, is_enrolled=True)
708
        request = get_mock_request(user)
709 710 711 712 713 714 715 716 717
        course_tab_list = get_course_tab_list(request, self.course)
        name_list = [x.name for x in course_tab_list]
        self.assertIn('Static Tab Free', name_list)
        self.assertNotIn('Static Tab Instructors Only', name_list)

        # Login as member of staff
        self.client.logout()
        staff_user = StaffFactory(course_key=self.course.id)
        self.client.login(username=staff_user.username, password='test')
718
        request = get_mock_request(staff_user)
719 720 721 722 723
        course_tab_list_staff = get_course_tab_list(request, self.course)
        name_list_staff = [x.name for x in course_tab_list_staff]
        self.assertIn('Static Tab Free', name_list_staff)
        self.assertIn('Static Tab Instructors Only', name_list_staff)

724

725
@attr(shard=1)
726
@pytest.mark.django111_expected_failure
727 728 729 730 731 732
class ProgressTestCase(TabTestCase):
    """Test cases for Progress Tab."""

    def check_progress_tab(self):
        """Helper function for verifying the progress tab."""
        return self.check_tab(
733 734
            tab_class=ProgressTab,
            dict_tab={'type': ProgressTab.type, 'name': 'same'},
735
            expected_link=self.reverse('progress', args=[self.course.id.to_deprecated_string()]),
736
            expected_tab_id=ProgressTab.type,
737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755
            invalid_dict_tab=None,
        )

    @patch('student.models.CourseEnrollment.is_enrolled')
    def test_progress(self, is_enrolled):
        is_enrolled.return_value = True
        self.course.hide_progress_tab = False
        tab = self.check_progress_tab()
        self.check_can_display_results(
            tab, for_staff_only=True, for_enrolled_users_only=True
        )

        self.course.hide_progress_tab = True
        self.check_progress_tab()
        self.check_can_display_results(
            tab, for_staff_only=True, for_enrolled_users_only=True, expected_value=False
        )


756
@attr(shard=1)
757
@pytest.mark.django111_expected_failure
758 759 760 761 762 763 764 765
class StaticTabTestCase(TabTestCase):
    """Test cases for Static Tab."""

    def test_static_tab(self):

        url_slug = 'schmug'

        tab = self.check_tab(
766 767
            tab_class=xmodule_tabs.StaticTab,
            dict_tab={'type': xmodule_tabs.StaticTab.type, 'name': 'same', 'url_slug': url_slug},
768 769 770 771 772 773 774 775
            expected_link=self.reverse('static_tab', args=[self.course.id.to_deprecated_string(), url_slug]),
            expected_tab_id='static_tab_schmug',
            invalid_dict_tab=self.fake_dict_tab,
        )
        self.check_can_display_results(tab)
        self.check_get_and_set_method_for_key(tab, 'url_slug')


776
@attr(shard=1)
777
@pytest.mark.django111_expected_failure
778 779 780 781 782 783
class CourseInfoTabTestCase(TabTestCase):
    """Test cases for the course info tab."""
    def setUp(self):
        self.user = self.create_mock_user()
        self.request = get_mock_request(self.user)

784
    @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=False)
785 786 787 788 789
    def test_default_tab(self):
        # Verify that the course info tab is the first tab
        tabs = get_course_tab_list(self.request, self.course)
        self.assertEqual(tabs[0].type, 'course_info')

790
    @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
791
    def test_default_tab_for_new_course_experience(self):
792 793 794 795
        # Verify that the unified course experience hides the course info tab
        tabs = get_course_tab_list(self.request, self.course)
        self.assertEqual(tabs[0].type, 'courseware')

796 797 798 799 800 801 802 803
    # TODO: LEARNER-611 - remove once course_info is removed.
    @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
    def test_default_tab_for_displayable(self):
        tabs = xmodule_tabs.CourseTabList.iterate_displayable(self.course, self.user)
        for i, tab in enumerate(tabs):
            if i == 0:
                self.assertEqual(tab.type, 'course_info')

804 805

@attr(shard=1)
806
@pytest.mark.django111_expected_failure
807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823
class DiscussionLinkTestCase(TabTestCase):
    """Test cases for discussion link tab."""

    def setUp(self):
        super(DiscussionLinkTestCase, self).setUp()

        self.tabs_with_discussion = [
            xmodule_tabs.CourseTab.load('discussion'),
        ]
        self.tabs_without_discussion = [
        ]

    @staticmethod
    def _reverse(course):
        """Custom reverse function"""
        def reverse_discussion_link(viewname, args):
            """reverse lookup for discussion link"""
824
            if viewname == "discussion.views.forum_form_discussion" and args == [unicode(course.id)]:
825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910
                return "default_discussion_link"
        return reverse_discussion_link

    def check_discussion(
            self, tab_list,
            expected_discussion_link,
            expected_can_display_value,
            discussion_link_in_course="",
            is_staff=True,
            is_enrolled=True,
    ):
        """Helper function to verify whether the discussion tab exists and can be displayed"""
        self.course.tabs = tab_list
        self.course.discussion_link = discussion_link_in_course
        discussion_tab = xmodule_tabs.CourseTabList.get_discussion(self.course)
        user = self.create_mock_user(is_authenticated=True, is_staff=is_staff, is_enrolled=is_enrolled)
        with patch('student.models.CourseEnrollment.is_enrolled') as check_is_enrolled:
            check_is_enrolled.return_value = is_enrolled
            self.assertEquals(
                (
                    discussion_tab is not None and
                    self.is_tab_enabled(discussion_tab, self.course, user) and
                    (discussion_tab.link_func(self.course, self._reverse(self.course)) == expected_discussion_link)
                ),
                expected_can_display_value
            )

    @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": False})
    def test_explicit_discussion_link(self):
        """Test that setting discussion_link overrides everything else"""
        self.check_discussion(
            tab_list=self.tabs_with_discussion,
            discussion_link_in_course="other_discussion_link",
            expected_discussion_link="other_discussion_link",
            expected_can_display_value=True,
        )

    @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": False})
    def test_discussions_disabled(self):
        """Test that other cases return None with discussions disabled"""
        for tab_list in [[], self.tabs_with_discussion, self.tabs_without_discussion]:
            self.check_discussion(
                tab_list=tab_list,
                expected_discussion_link=not None,
                expected_can_display_value=False,
            )

    @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
    def test_tabs_with_discussion(self):
        """Test a course with a discussion tab configured"""
        self.check_discussion(
            tab_list=self.tabs_with_discussion,
            expected_discussion_link="default_discussion_link",
            expected_can_display_value=True,
        )

    @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
    def test_tabs_without_discussion(self):
        """Test a course with tabs configured but without a discussion tab"""
        self.check_discussion(
            tab_list=self.tabs_without_discussion,
            expected_discussion_link=not None,
            expected_can_display_value=False,
        )

    @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
    def test_tabs_enrolled_or_staff(self):
        for is_enrolled, is_staff in [(True, False), (False, True)]:
            self.check_discussion(
                tab_list=self.tabs_with_discussion,
                expected_discussion_link="default_discussion_link",
                expected_can_display_value=True,
                is_enrolled=is_enrolled,
                is_staff=is_staff
            )

    @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
    def test_tabs_not_enrolled_or_staff(self):
        is_enrolled = is_staff = False
        self.check_discussion(
            tab_list=self.tabs_with_discussion,
            expected_discussion_link="default_discussion_link",
            expected_can_display_value=False,
            is_enrolled=is_enrolled,
            is_staff=is_staff
        )