From 90fe71dbfe92d7bcd0647d1f6b4e6391d48cb131 Mon Sep 17 00:00:00 2001 From: wajeeha-khalid Date: Tue, 15 Sep 2015 17:01:46 +0500 Subject: [PATCH] MA-1248 - CourseEnrollmentAPI: added discussion URL --- common/djangoapps/xmodule_django/models.py | 1 - lms/djangoapps/discussion_api/api.py | 4 +- .../discussion_api/tests/test_api.py | 24 +++---- .../discussion_api/tests/test_views.py | 11 +++ .../django_comment_client/forum/views.py | 7 +- lms/djangoapps/django_comment_client/utils.py | 12 ++++ .../mobile_api/users/serializers.py | 7 ++ lms/djangoapps/mobile_api/users/tests.py | 18 ++++- lms/djangoapps/mobile_api/users/views.py | 2 + .../0007_auto__add_courseoverviewtab.py | 68 +++++++++++++++++++ .../content/course_overviews/models.py | 29 +++++++- .../content/course_overviews/tests.py | 8 +++ 12 files changed, 167 insertions(+), 24 deletions(-) create mode 100644 openedx/core/djangoapps/content/course_overviews/migrations/0007_auto__add_courseoverviewtab.py diff --git a/common/djangoapps/xmodule_django/models.py b/common/djangoapps/xmodule_django/models.py index 3a457a44a6..99268d7e4f 100644 --- a/common/djangoapps/xmodule_django/models.py +++ b/common/djangoapps/xmodule_django/models.py @@ -1,7 +1,6 @@ """ Useful django models for implementing XBlock infrastructure in django. """ - import warnings from django.db import models diff --git a/lms/djangoapps/discussion_api/api.py b/lms/djangoapps/discussion_api/api.py index 86d4d4ca8b..12dd914bd1 100644 --- a/lms/djangoapps/discussion_api/api.py +++ b/lms/djangoapps/discussion_api/api.py @@ -13,8 +13,8 @@ from rest_framework.exceptions import PermissionDenied from opaque_keys import InvalidKeyError from opaque_keys.edx.locator import CourseKey - from courseware.courses import get_course_with_access + from discussion_api.forms import CommentActionsForm, ThreadActionsForm from discussion_api.pagination import get_paginated_data from discussion_api.permissions import ( @@ -55,7 +55,7 @@ def _get_course_or_404(course_key, user): disabled for the course. """ course = get_course_with_access(user, 'load', course_key, check_if_enrolled=True) - if not any([tab.type == 'discussion' for tab in course.tabs]): + if not any([tab.type == 'discussion' and tab.is_enabled(course, user) for tab in course.tabs]): raise Http404 return course diff --git a/lms/djangoapps/discussion_api/tests/test_api.py b/lms/djangoapps/discussion_api/tests/test_api.py index e61a1202b6..7168f7d159 100644 --- a/lms/djangoapps/discussion_api/tests/test_api.py +++ b/lms/djangoapps/discussion_api/tests/test_api.py @@ -81,11 +81,11 @@ def _discussion_disabled_course_for(user): @ddt.ddt +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class GetCourseTest(UrlResetMixin, SharedModuleStoreTestCase): """Test for get_course""" @classmethod - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUpClass(cls): super(GetCourseTest, cls).setUpClass() cls.course = CourseFactory.create(org="x", course="y", run="z") @@ -154,9 +154,9 @@ class GetCourseTest(UrlResetMixin, SharedModuleStoreTestCase): @mock.patch.dict("django.conf.settings.FEATURES", {"DISABLE_START_DATES": False}) +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class GetCourseTopicsTest(UrlResetMixin, ModuleStoreTestCase): """Test for get_course_topics""" - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUp(self): super(GetCourseTopicsTest, self).setUp() @@ -480,11 +480,11 @@ class GetCourseTopicsTest(UrlResetMixin, ModuleStoreTestCase): @ddt.ddt +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, SharedModuleStoreTestCase): """Test for get_thread_list""" @classmethod - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUpClass(cls): super(GetThreadListTest, cls).setUpClass() cls.course = CourseFactory.create() @@ -909,15 +909,16 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, SharedModuleSto @ddt.ddt +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class GetCommentListTest(CommentsServiceMockMixin, SharedModuleStoreTestCase): """Test for get_comment_list""" @classmethod - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUpClass(cls): super(GetCommentListTest, cls).setUpClass() cls.course = CourseFactory.create() + @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUp(self): super(GetCommentListTest, self).setUp() httpretty.reset() @@ -1333,6 +1334,7 @@ class GetCommentListTest(CommentsServiceMockMixin, SharedModuleStoreTestCase): @ddt.ddt @disable_signal(api, 'thread_created') @disable_signal(api, 'thread_voted') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class CreateThreadTest( CommentsServiceMockMixin, UrlResetMixin, @@ -1341,7 +1343,6 @@ class CreateThreadTest( ): """Tests for create_thread""" @classmethod - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUpClass(cls): super(CreateThreadTest, cls).setUpClass() cls.course = CourseFactory.create() @@ -1585,6 +1586,7 @@ class CreateThreadTest( @ddt.ddt @disable_signal(api, 'comment_created') @disable_signal(api, 'comment_voted') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class CreateCommentTest( CommentsServiceMockMixin, UrlResetMixin, @@ -1593,7 +1595,6 @@ class CreateCommentTest( ): """Tests for create_comment""" @classmethod - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUpClass(cls): super(CreateCommentTest, cls).setUpClass() cls.course = CourseFactory.create() @@ -1859,6 +1860,7 @@ class CreateCommentTest( @ddt.ddt @disable_signal(api, 'thread_edited') @disable_signal(api, 'thread_voted') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class UpdateThreadTest( CommentsServiceMockMixin, UrlResetMixin, @@ -1867,7 +1869,6 @@ class UpdateThreadTest( ): """Tests for update_thread""" @classmethod - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUpClass(cls): super(UpdateThreadTest, cls).setUpClass() cls.course = CourseFactory.create() @@ -2247,6 +2248,7 @@ class UpdateThreadTest( @ddt.ddt @disable_signal(api, 'comment_edited') @disable_signal(api, 'comment_voted') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class UpdateCommentTest( CommentsServiceMockMixin, UrlResetMixin, @@ -2256,7 +2258,6 @@ class UpdateCommentTest( """Tests for update_comment""" @classmethod - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUpClass(cls): super(UpdateCommentTest, cls).setUpClass() cls.course = CourseFactory.create() @@ -2304,7 +2305,6 @@ class UpdateCommentTest( self.register_get_comment_response(cs_comment_data) self.register_put_comment_response(cs_comment_data) - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def test_empty(self): """Check that an empty update does not make any modifying requests.""" self.register_comment() @@ -2632,6 +2632,7 @@ class UpdateCommentTest( @ddt.ddt @disable_signal(api, 'thread_deleted') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class DeleteThreadTest( CommentsServiceMockMixin, UrlResetMixin, @@ -2640,7 +2641,6 @@ class DeleteThreadTest( ): """Tests for delete_thread""" @classmethod - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUpClass(cls): super(DeleteThreadTest, cls).setUpClass() cls.course = CourseFactory.create() @@ -2771,6 +2771,7 @@ class DeleteThreadTest( @ddt.ddt @disable_signal(api, 'comment_deleted') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class DeleteCommentTest( CommentsServiceMockMixin, UrlResetMixin, @@ -2779,7 +2780,6 @@ class DeleteCommentTest( ): """Tests for delete_comment""" @classmethod - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUpClass(cls): super(DeleteCommentTest, cls).setUpClass() cls.course = CourseFactory.create() @@ -2928,6 +2928,7 @@ class DeleteCommentTest( @ddt.ddt +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class RetrieveThreadTest( CommentsServiceMockMixin, UrlResetMixin, @@ -2935,7 +2936,6 @@ class RetrieveThreadTest( ): """Tests for get_thread""" @classmethod - @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUpClass(cls): super(RetrieveThreadTest, cls).setUpClass() cls.course = CourseFactory.create() diff --git a/lms/djangoapps/discussion_api/tests/test_views.py b/lms/djangoapps/discussion_api/tests/test_views.py index 0809df43e2..9556ad9f8c 100644 --- a/lms/djangoapps/discussion_api/tests/test_views.py +++ b/lms/djangoapps/discussion_api/tests/test_views.py @@ -70,6 +70,7 @@ class DiscussionAPIViewTestMixin(CommentsServiceMockMixin, UrlResetMixin): ) +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for CourseView""" def setUp(self): @@ -103,6 +104,7 @@ class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ) +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for CourseTopicsView""" def setUp(self): @@ -139,6 +141,7 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): @ddt.ddt @httpretty.activate +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for ThreadViewSet list""" def setUp(self): @@ -388,6 +391,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): @httpretty.activate @disable_signal(api, 'thread_created') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for ThreadViewSet create""" def setUp(self): @@ -480,6 +484,7 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): @httpretty.activate @disable_signal(api, 'thread_edited') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for ThreadViewSet partial_update""" def setUp(self): @@ -580,6 +585,7 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest @httpretty.activate @disable_signal(api, 'thread_deleted') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class ThreadViewSetDeleteTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for ThreadViewSet delete""" def setUp(self): @@ -613,6 +619,7 @@ class ThreadViewSetDeleteTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): @httpretty.activate +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for CommentViewSet list""" def setUp(self): @@ -744,6 +751,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): @httpretty.activate @disable_signal(api, 'comment_deleted') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class CommentViewSetDeleteTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for ThreadViewSet delete""" @@ -785,6 +793,7 @@ class CommentViewSetDeleteTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): @httpretty.activate @disable_signal(api, 'comment_created') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for CommentViewSet create""" def setUp(self): @@ -869,6 +878,7 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): @disable_signal(api, 'comment_edited') +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for CommentViewSet partial_update""" def setUp(self): @@ -954,6 +964,7 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes @httpretty.activate +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class ThreadViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for ThreadViewSet Retrieve""" def setUp(self): diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 484bccd7d8..2d95fd89b3 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -27,7 +27,6 @@ from openedx.core.djangoapps.course_groups.cohorts import ( from courseware.tabs import EnrolledTab from courseware.access import has_access from xmodule.modulestore.django import modulestore -from ccx.overrides import get_current_ccx from django_comment_common.utils import ThreadContext from django_comment_client.permissions import has_permission, get_team @@ -66,11 +65,7 @@ class DiscussionTab(EnrolledTab): def is_enabled(cls, course, user=None): if not super(DiscussionTab, cls).is_enabled(course, user): return False - - if settings.FEATURES.get('CUSTOM_COURSES_EDX', False): - if get_current_ccx(course.id): - return False - return settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE') + return utils.is_discussion_enabled(course.id) def _attr_safe_json(obj): diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 0374c770de..f7921e0f9b 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -2,6 +2,7 @@ from collections import defaultdict from datetime import datetime import json import logging +from django.conf import settings import pytz from django.contrib.auth.models import User @@ -13,6 +14,7 @@ import pystache_custom as pystache from opaque_keys.edx.locations import i4xEncoder from opaque_keys.edx.keys import CourseKey from xmodule.modulestore.django import modulestore +from ccx.overrides import get_current_ccx from django_comment_common.models import Role, FORUM_ROLE_STUDENT from django_comment_client.permissions import check_permissions_by_view, has_permission, get_team @@ -716,3 +718,13 @@ def is_commentable_cohorted(course_key, commentable_id): log.debug(u"is_commentable_cohorted(%s, %s) = {%s}", course_key, commentable_id, ans) return ans + + +def is_discussion_enabled(course_id): + """ + Return True if Discussion is enabled for a course; else False + """ + if settings.FEATURES.get('CUSTOM_COURSES_EDX', False): + if get_current_ccx(course_id): + return False + return settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE') diff --git a/lms/djangoapps/mobile_api/users/serializers.py b/lms/djangoapps/mobile_api/users/serializers.py index cb2d0a2782..a981861ced 100644 --- a/lms/djangoapps/mobile_api/users/serializers.py +++ b/lms/djangoapps/mobile_api/users/serializers.py @@ -34,10 +34,16 @@ class CourseOverviewField(serializers.RelatedField): kwargs={'course_id': course_id}, request=request ) + discussion_url = reverse( + 'discussion_course', + kwargs={'course_id': course_id}, + request=request + ) if course_overview.is_discussion_tab_enabled() else None else: video_outline_url = None course_updates_url = None course_handouts_url = None + discussion_url = None if course_overview.advertised_start is not None: start_type = "string" @@ -68,6 +74,7 @@ class CourseOverviewField(serializers.RelatedField): "video_outline": video_outline_url, "course_updates": course_updates_url, "course_handouts": course_handouts_url, + "discussion_url": discussion_url, "subscription_id": course_overview.clean_id(padding_char='_'), "courseware_access": has_access(request.user, 'load_mobile', course_overview).to_json() if request else None } diff --git a/lms/djangoapps/mobile_api/users/tests.py b/lms/djangoapps/mobile_api/users/tests.py index 45fd9801ba..14d1c44e47 100644 --- a/lms/djangoapps/mobile_api/users/tests.py +++ b/lms/djangoapps/mobile_api/users/tests.py @@ -28,6 +28,7 @@ from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory from .. import errors from ..testutils import MobileAPITestCase, MobileAuthTestMixin, MobileAuthUserTestMixin, MobileCourseAccessTestMixin from .serializers import CourseEnrollmentSerializer +from util.testing import UrlResetMixin class TestUserDetailApi(MobileAPITestCase, MobileAuthUserTestMixin): @@ -60,7 +61,7 @@ class TestUserInfoApi(MobileAPITestCase, MobileAuthTestMixin): @ddt.ddt -class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin): +class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTestMixin): """ Tests for /api/mobile/v0.5/users//course_enrollments/ """ @@ -71,7 +72,14 @@ class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin): LAST_WEEK = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=7) ADVERTISED_START = "Spring 2016" + @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) + def setUp(self, *args, **kwargs): + super(TestUserEnrollmentApi, self).setUp() + def verify_success(self, response): + """ + Verifies user course enrollment response for success + """ super(TestUserEnrollmentApi, self).verify_success(response) courses = response.data self.assertEqual(len(courses), 1) @@ -205,6 +213,14 @@ class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin): course_data = response.data[0]['course'] self.assertEquals(course_data['social_urls']['facebook'], self.course.facebook_url) + @patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) + def test_discussion_url(self): + self.login_and_enroll() + + response = self.api_response() + response_discussion_url = response.data[0]['course']['discussion_url'] # pylint: disable=E1101 + self.assertIn('/api/discussion/v1/courses/{}'.format(self.course.id), response_discussion_url) + class CourseStatusAPITestCase(MobileAPITestCase): """ diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py index 2b695ecca7..a5f5825427 100644 --- a/lms/djangoapps/mobile_api/users/views.py +++ b/lms/djangoapps/mobile_api/users/views.py @@ -239,6 +239,8 @@ class UserCourseEnrollmentsList(generics.ListAPIView): the course. * video_outline: The URI to get the list of all videos that the user can access in the course. + * discussion_url: The URI to access data for course discussions if + it is enabled, otherwise null. * created: The date the course was created. * is_active: Whether the course is currently active. Possible values diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0007_auto__add_courseoverviewtab.py b/openedx/core/djangoapps/content/course_overviews/migrations/0007_auto__add_courseoverviewtab.py new file mode 100644 index 0000000000..70f4ca08c0 --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/migrations/0007_auto__add_courseoverviewtab.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'CourseOverviewTab' + db.create_table('course_overviews_courseoverviewtab', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('tab_id', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('course_overview', self.gf('django.db.models.fields.related.ForeignKey')(related_name='tabs', to=orm['course_overviews.CourseOverview'])), + )) + db.send_create_signal('course_overviews', ['CourseOverviewTab']) + + + def backwards(self, orm): + # Deleting model 'CourseOverviewTab' + db.delete_table('course_overviews_courseoverviewtab') + + + models = { + 'course_overviews.courseoverview': { + 'Meta': {'object_name': 'CourseOverview'}, + '_location': ('xmodule_django.models.UsageKeyField', [], {'max_length': '255'}), + '_pre_requisite_courses_json': ('django.db.models.fields.TextField', [], {}), + 'advertised_start': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'cert_html_view_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'cert_name_long': ('django.db.models.fields.TextField', [], {}), + 'cert_name_short': ('django.db.models.fields.TextField', [], {}), + 'certificates_display_behavior': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'certificates_show_before_end': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'course_image_url': ('django.db.models.fields.TextField', [], {}), + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'days_early_for_beta': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'display_name': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'display_number_with_default': ('django.db.models.fields.TextField', [], {}), + 'display_org_with_default': ('django.db.models.fields.TextField', [], {}), + 'end': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'end_of_course_survey_url': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'enrollment_domain': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'enrollment_end': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'enrollment_start': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'facebook_url': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'has_any_active_web_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'primary_key': 'True', 'db_index': 'True'}), + 'invitation_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'lowest_passing_grade': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '5', 'decimal_places': '2'}), + 'max_student_enrollments_allowed': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'mobile_available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), + 'social_sharing_url': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'start': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'version': ('django.db.models.fields.IntegerField', [], {}), + 'visible_to_staff_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'course_overviews.courseoverviewtab': { + 'Meta': {'object_name': 'CourseOverviewTab'}, + 'course_overview': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tabs'", 'to': "orm['course_overviews.CourseOverview']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'tab_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['course_overviews'] \ No newline at end of file diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index f5ec132f99..b9af2cf470 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -1,15 +1,17 @@ """ Declaration of CourseOverview model """ - import json +from django.db import models from django.db.models.fields import BooleanField, DateTimeField, DecimalField, TextField, FloatField, IntegerField from django.db.utils import IntegrityError from django.utils.translation import ugettext +from lms.djangoapps import django_comment_client from model_utils.models import TimeStampedModel from opaque_keys.edx.keys import CourseKey + from util.date_utils import strftime_localized from xmodule import course_metadata_utils from xmodule.course_module import CourseDescriptor @@ -30,7 +32,7 @@ class CourseOverview(TimeStampedModel): """ # IMPORTANT: Bump this whenever you modify this model and/or add a migration. - VERSION = 1 + VERSION = 2 # Cache entry versioning. version = IntegerField() @@ -176,6 +178,10 @@ class CourseOverview(TimeStampedModel): course_overview = cls._create_from_course(course) try: course_overview.save() + CourseOverviewTab.objects.bulk_create([ + CourseOverviewTab(tab_id=tab.tab_id, course_overview=course_overview) + for tab in course.tabs + ]) except IntegrityError: # There is a rare race condition that will occur if # CourseOverview.get_from_id is called while a @@ -358,3 +364,22 @@ class CourseOverview(TimeStampedModel): CourseKey.from_string(course_overview['id']) for course_overview in CourseOverview.objects.values('id') ] + + def is_discussion_tab_enabled(self): + """ + Returns True if course has discussion tab and is enabled + """ + tabs = self.tabs.all() # pylint: disable=E1101 + # creates circular import; hence explicitly referenced is_discussion_enabled + for tab in tabs: + if tab.tab_id == "discussion" and django_comment_client.utils.is_discussion_enabled(self.id): + return True + return False + + +class CourseOverviewTab(models.Model): + """ + Model for storing and caching tabs information of a course. + """ + tab_id = models.CharField(max_length=50) + course_overview = models.ForeignKey(CourseOverview, db_index=True, related_name="tabs") diff --git a/openedx/core/djangoapps/content/course_overviews/tests.py b/openedx/core/djangoapps/content/course_overviews/tests.py index ae323be440..7b1d33d410 100644 --- a/openedx/core/djangoapps/content/course_overviews/tests.py +++ b/openedx/core/djangoapps/content/course_overviews/tests.py @@ -34,6 +34,8 @@ class CourseOverviewTestCase(ModuleStoreTestCase): NEXT_WEEK = TODAY + datetime.timedelta(days=7) NEXT_MONTH = TODAY + datetime.timedelta(days=30) + COURSE_OVERVIEW_TABS = {'courseware', 'info', 'textbooks', 'discussion', 'wiki', 'progress'} + def check_course_overview_against_course(self, course): """ Compares a CourseOverview object against its corresponding @@ -164,6 +166,12 @@ class CourseOverviewTestCase(ModuleStoreTestCase): self.assertEqual(course_value, cache_miss_value) self.assertEqual(cache_miss_value, cache_hit_value) + # test tabs for both cached miss and cached hit courses + for course_overview in [course_overview_cache_miss, course_overview_cache_hit]: + course_overview_tabs = course_overview.tabs.all() + course_resp_tabs = {tab.tab_id for tab in course_overview_tabs} + self.assertEqual(self.COURSE_OVERVIEW_TABS, course_resp_tabs) + @ddt.data(*itertools.product( [ {