diff --git a/lms/djangoapps/course_home_api/outline/v1/views.py b/lms/djangoapps/course_home_api/outline/v1/views.py
index f79730a01e..b2a64703c1 100644
--- a/lms/djangoapps/course_home_api/outline/v1/views.py
+++ b/lms/djangoapps/course_home_api/outline/v1/views.py
@@ -30,13 +30,13 @@ from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course
from lms.djangoapps.courseware.date_summary import TodaysDate
from lms.djangoapps.courseware.masquerade import setup_masquerade
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
-from openedx.core.djangoapps.user_api.course_tag.api import get_course_tag, set_course_tag
from openedx.features.course_duration_limits.access import generate_course_expired_message
-from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, LATEST_UPDATE_FLAG
+from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG
from openedx.features.course_experience.course_tools import CourseToolsPluginManager
+from openedx.features.course_experience.course_updates import (
+ dismiss_current_update_for_user, get_current_update_for_user,
+)
from openedx.features.course_experience.utils import get_course_outline_block_tree
-from openedx.features.course_experience.views.latest_update import LatestUpdateFragmentView
-from openedx.features.course_experience.views.welcome_message import PREFERENCE_KEY, WelcomeMessageFragmentView
from openedx.features.discounts.utils import generate_offer_html
from common.djangoapps.student.models import CourseEnrollment
from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE
@@ -168,12 +168,7 @@ class OutlineTabView(RetrieveAPIView):
offer_html = show_enrolled and generate_offer_html(request.user, course_overview)
course_expired_html = show_enrolled and generate_course_expired_message(request.user, course_overview)
- welcome_message_html = None
- if show_enrolled:
- if LATEST_UPDATE_FLAG.is_enabled(course_key):
- welcome_message_html = LatestUpdateFragmentView().latest_update_html(request, course)
- elif get_course_tag(request.user, course_key, PREFERENCE_KEY) != 'False':
- welcome_message_html = WelcomeMessageFragmentView().welcome_message_html(request, course)
+ welcome_message_html = show_enrolled and get_current_update_for_user(request, course)
enroll_alert = {
'can_enroll': True,
@@ -284,7 +279,8 @@ def dismiss_welcome_message(request):
try:
course_key = CourseKey.from_string(course_id)
- set_course_tag(request.user, course_key, PREFERENCE_KEY, 'False')
+ course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
+ dismiss_current_update_for_user(request, course)
return Response({'message': _('Welcome message successfully dismissed.')})
except Exception:
raise UnableToDismissWelcomeMessage
diff --git a/openedx/features/course_experience/__init__.py b/openedx/features/course_experience/__init__.py
index bc8eb3140c..5599fbc619 100644
--- a/openedx/features/course_experience/__init__.py
+++ b/openedx/features/course_experience/__init__.py
@@ -72,7 +72,8 @@ UPGRADE_DEADLINE_MESSAGE = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'upgrade_dead
# .. toggle_creation_date: 2017-09-11
# .. toggle_target_removal_date: None
# .. toggle_warnings: This is meant to be configured using waffle_utils course override only. Either do not create the
-# actual waffle flag, or be sure to unset the flag even for Superusers.
+# actual waffle flag, or be sure to unset the flag even for Superusers. This is no longer used in the learning MFE
+# and can be removed when the outline tab is fully moved to the learning MFE.
# .. toggle_tickets: None
LATEST_UPDATE_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'latest_update', __name__)
diff --git a/openedx/features/course_experience/course_updates.py b/openedx/features/course_experience/course_updates.py
new file mode 100644
index 0000000000..37de393ca1
--- /dev/null
+++ b/openedx/features/course_experience/course_updates.py
@@ -0,0 +1,113 @@
+"""
+Utilities for course updates.
+"""
+
+import hashlib
+from datetime import datetime
+
+from lms.djangoapps.courseware.courses import get_course_info_section_module
+from openedx.core.djangoapps.user_api.course_tag.api import get_course_tag, set_course_tag
+
+STATUS_VISIBLE = 'visible'
+STATUS_DELETED = 'deleted'
+
+VIEW_WELCOME_MESSAGE_KEY = 'view-welcome-message'
+
+
+def _calculate_update_hash(update):
+ """
+ Returns a hash of the content of a course update. Does not need to be secure.
+ """
+ hasher = hashlib.md5()
+ hasher.update(update['content'].encode('utf-8'))
+ return hasher.hexdigest()
+
+
+def _get_dismissed_hashes(user, course_key):
+ """
+ Returns a list of dismissed hashes, or None if all updates have been dismissed.
+ """
+ view_welcome_message = get_course_tag(user, course_key, VIEW_WELCOME_MESSAGE_KEY)
+ if view_welcome_message == 'False': # legacy value, which dismisses all updates
+ return None
+ return view_welcome_message.split(',') if view_welcome_message else []
+
+
+def _add_dismissed_hash(user, course_key, new_hash):
+ """
+ Add a new hash to the list of previously dismissed updates.
+
+ Overwrites a 'False' value with the current hash. Though we likely won't end up in that situation, since
+ a 'False' value will never show the update to the user to dismiss in the first place.
+ """
+ hashes = _get_dismissed_hashes(user, course_key) or []
+ hashes.append(new_hash)
+ set_course_tag(user, course_key, VIEW_WELCOME_MESSAGE_KEY, ','.join(hashes))
+
+
+def _safe_parse_date(date):
+ """
+ Since this is used solely for ordering purposes, use today's date as a default
+ """
+ try:
+ return datetime.strptime(date, '%B %d, %Y')
+ except ValueError: # occurs for ill-formatted date values
+ return datetime.today()
+
+
+def get_ordered_updates(request, course):
+ """
+ Returns all public course updates in reverse chronological order, including dismissed ones.
+ """
+ info_module = get_course_info_section_module(request, request.user, course, 'updates')
+ if not info_module:
+ return []
+
+ info_block = getattr(info_module, '_xmodule', info_module)
+ ordered_updates = [update for update in info_module.items if update.get('status') == STATUS_VISIBLE]
+ ordered_updates.sort(
+ key=lambda item: (_safe_parse_date(item['date']), item['id']),
+ reverse=True
+ )
+ for update in ordered_updates:
+ update['content'] = info_block.system.replace_urls(update['content'])
+ return ordered_updates
+
+
+def get_current_update_for_user(request, course):
+ """
+ Returns the current (most recent) course update HTML.
+
+ Some rules about when we show updates:
+ - If the newest update has not been dismissed yet, it gets returned.
+ - If the newest update has been dismissed, we will return None.
+ - Will return a previously-dismissed newest update if it has been edited since being dismissed.
+ - If a current update is deleted and an already dismissed update is now the newest one, we don't want to show that.
+ """
+ updates = get_ordered_updates(request, course)
+ if not updates:
+ return None
+
+ dismissed_hashes = _get_dismissed_hashes(request.user, course.id)
+ if dismissed_hashes is None: # all updates dismissed
+ return None
+
+ update_hash = _calculate_update_hash(updates[0])
+ if update_hash in dismissed_hashes:
+ return None
+
+ return updates[0]['content']
+
+
+def dismiss_current_update_for_user(request, course):
+ """
+ Marks the current course update for this user as dismissed.
+
+ See get_current_update_for_user for what "current course update" means in practice.
+ """
+ updates = get_ordered_updates(request, course)
+ if not updates:
+ return None
+
+ update_hash = _calculate_update_hash(updates[0])
+ _add_dismissed_hash(request.user, course.id, update_hash)
diff --git a/openedx/features/course_experience/tests/__init__.py b/openedx/features/course_experience/tests/__init__.py
index e69de29bb2..7eb664cf7a 100644
--- a/openedx/features/course_experience/tests/__init__.py
+++ b/openedx/features/course_experience/tests/__init__.py
@@ -0,0 +1,104 @@
+"""
+Common test code for course_experience, like shared base classes.
+"""
+
+from common.djangoapps.student.models import CourseEnrollment
+from common.djangoapps.student.tests.factories import UserFactory
+from lms.djangoapps.courseware.courses import get_course_info_usage_key
+from xmodule.modulestore import ModuleStoreEnum
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.exceptions import ItemNotFoundError
+from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
+from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
+
+
+class BaseCourseUpdatesTestCase(SharedModuleStoreTestCase):
+ """Base class for working with course updates."""
+ @classmethod
+ def setUpClass(cls):
+ """Set up the simplest course possible."""
+ # pylint: disable=super-method-not-called
+ with super().setUpClassAndTestData():
+ with cls.store.default_store(ModuleStoreEnum.Type.split):
+ cls.course = CourseFactory.create()
+ with cls.store.bulk_operations(cls.course.id):
+ # Create a basic course structure
+ chapter = ItemFactory.create(category='chapter', parent_location=cls.course.location)
+ section = ItemFactory.create(category='sequential', parent_location=chapter.location)
+ ItemFactory.create(category='vertical', parent_location=section.location)
+
+ @classmethod
+ def setUpTestData(cls):
+ """Set up and enroll our fake user in the course."""
+ super().setUpTestData()
+ cls.user = UserFactory(password=cls.TEST_PASSWORD)
+ CourseEnrollment.enroll(cls.user, cls.course.id)
+
+ def setUp(self):
+ super().setUp()
+ self.client.login(username=self.user.username, password=self.TEST_PASSWORD)
+
+ def tearDown(self):
+ self.remove_course_updates()
+ super().tearDown()
+
+ def remove_course_updates(self, user=None, course=None):
+ """Remove any course updates in the specified course."""
+ user = user or self.user
+ course = course or self.course
+ updates_usage_key = get_course_info_usage_key(course, 'updates')
+ try:
+ course_updates = modulestore().get_item(updates_usage_key)
+ modulestore().delete_item(course_updates.location, user.id)
+ except (ItemNotFoundError, ValueError):
+ pass
+
+ def edit_course_update(self, index, content=None, course=None, user=None, date=None, deleted=None):
+ """Edits a course update item. Only changes explicitly provided parameters."""
+ user = user or self.user
+ course = course or self.course
+ updates_usage_key = get_course_info_usage_key(course, 'updates')
+ course_updates = modulestore().get_item(updates_usage_key)
+ for item in course_updates.items:
+ if item['id'] == index:
+ if date is not None:
+ item['date'] = date
+ if content is not None:
+ item['content'] = content
+ if deleted is not None:
+ item['status'] = 'deleted' if deleted else 'visible'
+ break
+ modulestore().update_item(course_updates, user.id)
+
+ def create_course_update(self, content, course=None, user=None, date='December 31, 1999', deleted=False):
+ """Creates a test welcome message for the specified course."""
+ user = user or self.user
+ course = course or self.course
+ updates_usage_key = get_course_info_usage_key(course, 'updates')
+ try:
+ course_updates = modulestore().get_item(updates_usage_key)
+ except ItemNotFoundError:
+ course_updates = self.create_course_updates_block(course=course, user=user)
+ item = {
+ 'id': len(course_updates.items) + 1,
+ 'date': date,
+ 'content': content,
+ 'status': 'deleted' if deleted else 'visible',
+ }
+ course_updates.items.append(item)
+ modulestore().update_item(course_updates, user.id)
+ return item
+
+ def create_course_updates_block(self, course=None, user=None):
+ """Create a course updates block."""
+ user = user or self.user
+ course = course or self.course
+ updates_usage_key = get_course_info_usage_key(course, 'updates')
+ course_updates = modulestore().create_item(
+ user.id,
+ updates_usage_key.course_key,
+ updates_usage_key.block_type,
+ block_id=updates_usage_key.block_id
+ )
+ course_updates.data = ''
+ return course_updates
diff --git a/openedx/features/course_experience/tests/test_course_updates.py b/openedx/features/course_experience/tests/test_course_updates.py
new file mode 100644
index 0000000000..7646788152
--- /dev/null
+++ b/openedx/features/course_experience/tests/test_course_updates.py
@@ -0,0 +1,121 @@
+"""
+Tests for the course updates utility methods.
+"""
+
+from django.test.client import RequestFactory
+
+from openedx.core.djangoapps.user_api.course_tag.api import get_course_tag, set_course_tag
+from openedx.features.course_experience.course_updates import (
+ dismiss_current_update_for_user, get_current_update_for_user, get_ordered_updates,
+)
+from openedx.features.course_experience.tests import BaseCourseUpdatesTestCase
+
+
+class TestCourseUpdatesUtils(BaseCourseUpdatesTestCase):
+ """Tests for the course update utility methods."""
+
+ UPDATES_TAG = 'view-welcome-message'
+
+ @classmethod
+ def setUpTestData(cls):
+ super().setUpTestData()
+ cls.request = RequestFactory().get('/')
+ cls.request.user = cls.user
+
+ def test_update_structure(self):
+ """Test that returned item dictionary is as we expect."""
+ content = 'HTML Content'
+ date = 'January 1, 2000'
+ self.create_course_update(content, date=date)
+ updates = get_ordered_updates(self.request, self.course)
+ self.assertListEqual(updates, [{
+ 'id': 1,
+ 'content': content,
+ 'date': date,
+ 'status': 'visible',
+ }])
+
+ def test_ordered_updates(self):
+ """Test that order of returned items follows our rules."""
+ first = self.create_course_update('2000', date='January 1, 2000')
+ second = self.create_course_update('2017', date='January 1, 2017')
+ third = self.create_course_update('Also 2017', date='January 1, 2017')
+ injected = self.create_course_update('Injected out of order', date='January 1, 2010')
+ ill_formed = self.create_course_update('Ill-formed date is parsed as now()', date='foobar')
+ self.create_course_update('Deleted is ignored', deleted=True)
+ updates = get_ordered_updates(self.request, self.course)
+ self.assertListEqual(updates, [ill_formed, third, second, injected, first])
+
+ def test_replace_urls(self):
+ """We should be replacing static URLs with course specific ones."""
+ self.create_course_update("
")
+ updates = get_ordered_updates(self.request, self.course)
+ expected = "
".format(
+ org=self.course.id.org,
+ course=self.course.id.course,
+ run=self.course.id.run,
+ )
+ self.assertEqual(updates[0]['content'], expected)
+
+ def test_ordered_update_includes_dismissed_updates(self):
+ """Ordered update list should still have dismissed updates."""
+ self.create_course_update('Dismissed')
+ dismiss_current_update_for_user(self.request, self.course)
+ updates = get_ordered_updates(self.request, self.course)
+ self.assertEqual(len(updates), 1)
+
+ def test_get_current_update_is_newest(self):
+ """Tests that the current update is also the newest."""
+ self.create_course_update('Oldest', date='January 1, 1900')
+ self.create_course_update('New', date='January 1, 2017')
+ self.create_course_update('Oldish', date='January 1, 2000')
+ self.assertEqual(get_current_update_for_user(self.request, self.course), 'New')
+
+ def test_get_current_update_when_dismissed(self):
+ """Tests that a dismissed update is not returned."""
+ self.create_course_update('Dismissed')
+ dismiss_current_update_for_user(self.request, self.course)
+ self.assertIsNone(get_current_update_for_user(self.request, self.course))
+
+ def test_get_current_update_when_dismissed_but_edited(self):
+ """Tests that a dismissed but edited update is returned."""
+ self.create_course_update('Original')
+ dismiss_current_update_for_user(self.request, self.course)
+ self.assertIsNone(get_current_update_for_user(self.request, self.course))
+ self.edit_course_update(1, content='Edited')
+ self.assertIsNotNone(get_current_update_for_user(self.request, self.course))
+
+ def test_get_current_update_remembers_dismissals(self):
+ """Tests that older dismissed updates are remembered."""
+ self.create_course_update('First')
+ self.create_course_update('Second')
+ dismiss_current_update_for_user(self.request, self.course)
+ self.create_course_update('Third')
+ dismiss_current_update_for_user(self.request, self.course)
+ self.create_course_update('Fourth')
+
+ self.assertEqual(get_current_update_for_user(self.request, self.course), 'Fourth')
+ self.edit_course_update(4, deleted=True)
+ self.assertIsNone(get_current_update_for_user(self.request, self.course))
+ self.edit_course_update(3, deleted=True)
+ self.assertIsNone(get_current_update_for_user(self.request, self.course))
+ self.edit_course_update(2, deleted=True)
+ self.assertEqual(get_current_update_for_user(self.request, self.course), 'First')
+
+ def test_legacy_ignore_all_support(self):
+ """Storing 'False' as the dismissal ignores all updates."""
+ self.create_course_update('First')
+ self.assertEqual(get_current_update_for_user(self.request, self.course), 'First')
+
+ set_course_tag(self.user, self.course.id, self.UPDATES_TAG, 'False')
+ self.assertIsNone(get_current_update_for_user(self.request, self.course))
+
+ def test_dismissal_hashing(self):
+ """Confirm that the stored dismissal values are what we expect, to catch accidentally changing formats."""
+ self.create_course_update('First')
+ dismiss_current_update_for_user(self.request, self.course)
+ self.create_course_update('Second')
+ dismiss_current_update_for_user(self.request, self.course)
+
+ tag = get_course_tag(self.user, self.course.id, self.UPDATES_TAG)
+ self.assertEqual(tag, '7fb55ed0b7a30342ba6da306428cae04,c22cf8376b1893dcfcef0649fe1a7d87')
diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py
index 5c727ac54a..389d2f7e90 100644
--- a/openedx/features/course_experience/tests/views/test_course_home.py
+++ b/openedx/features/course_experience/tests/views/test_course_home.py
@@ -54,6 +54,7 @@ from openedx.features.course_experience import (
SHOW_REVIEWS_TOOL_FLAG,
SHOW_UPGRADE_MSG_ON_COURSE_HOME
)
+from openedx.features.course_experience.tests import BaseCourseUpdatesTestCase
from openedx.features.discounts.applicability import get_discount_expiration_date
from openedx.features.discounts.utils import REV1008_EXPERIMENT_ID, format_strikeout_price
from common.djangoapps.student.models import CourseEnrollment, FBEEnrollmentExclusion
@@ -61,12 +62,11 @@ from common.djangoapps.student.tests.factories import UserFactory
from common.djangoapps.util.date_utils import strftime_localized
from xmodule.course_module import COURSE_VISIBILITY_PRIVATE, COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE
from xmodule.modulestore import ModuleStoreEnum
-from xmodule.modulestore.tests.django_utils import CourseUserType, ModuleStoreTestCase, SharedModuleStoreTestCase
+from xmodule.modulestore.tests.django_utils import CourseUserType, ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from ... import COURSE_PRE_START_ACCESS_FLAG, ENABLE_COURSE_GOALS
from .helpers import add_course_mode, remove_course_mode
-from .test_course_updates import create_course_update, remove_course_updates
TEST_PASSWORD = 'test'
TEST_CHAPTER_NAME = 'Test Chapter'
@@ -113,7 +113,7 @@ def course_home_url_from_string(course_key_string):
)
-class CourseHomePageTestCase(SharedModuleStoreTestCase):
+class CourseHomePageTestCase(BaseCourseUpdatesTestCase):
"""
Base class for testing the course home page.
"""
@@ -154,10 +154,8 @@ class CourseHomePageTestCase(SharedModuleStoreTestCase):
@classmethod
def setUpTestData(cls):
"""Set up and enroll our fake user in the course."""
- super(CourseHomePageTestCase, cls).setUpTestData()
+ super().setUpTestData()
cls.staff_user = StaffFactory(course_key=cls.course.id, password=TEST_PASSWORD)
- cls.user = UserFactory(password=TEST_PASSWORD)
- CourseEnrollment.enroll(cls.user, cls.course.id)
def create_future_course(self, specific_date=None):
"""
@@ -170,17 +168,9 @@ class CourseHomePageTestCase(SharedModuleStoreTestCase):
class TestCourseHomePage(CourseHomePageTestCase):
- def setUp(self):
- super(TestCourseHomePage, self).setUp()
- self.client.login(username=self.user.username, password=TEST_PASSWORD)
-
- def tearDown(self):
- remove_course_updates(self.user, self.course)
- super(TestCourseHomePage, self).tearDown()
-
def test_welcome_message_when_unified(self):
# Create a welcome message
- create_course_update(self.course, self.user, TEST_WELCOME_MESSAGE)
+ self.create_course_update(TEST_WELCOME_MESSAGE)
url = course_home_url(self.course)
response = self.client.get(url)
@@ -189,7 +179,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
@override_waffle_flag(DISABLE_UNIFIED_COURSE_TAB_FLAG, active=True)
def test_welcome_message_when_not_unified(self):
# Create a welcome message
- create_course_update(self.course, self.user, TEST_WELCOME_MESSAGE)
+ self.create_course_update(TEST_WELCOME_MESSAGE)
url = course_home_url(self.course)
response = self.client.get(url)
@@ -204,7 +194,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
response = self.client.get(url)
self.assertNotContains(response, TEST_COURSE_UPDATES_TOOL, status_code=200)
- create_course_update(self.course, self.user, TEST_UPDATE_MESSAGE)
+ self.create_course_update(TEST_UPDATE_MESSAGE)
url = course_home_url(self.course)
response = self.client.get(url)
self.assertContains(response, TEST_COURSE_UPDATES_TOOL, status_code=200)
@@ -249,18 +239,15 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
"""
def setUp(self):
- super(TestCourseHomePageAccess, self).setUp()
+ super().setUp()
+ self.client.logout() # start with least access and add access back in the various test cases
# Make this a verified course so that an upgrade message might be shown
add_course_mode(self.course, mode_slug=CourseMode.AUDIT)
add_course_mode(self.course)
# Add a welcome message
- create_course_update(self.course, self.staff_user, TEST_WELCOME_MESSAGE)
-
- def tearDown(self):
- remove_course_updates(self.staff_user, self.course)
- super(TestCourseHomePageAccess, self).tearDown()
+ self.create_course_update(TEST_WELCOME_MESSAGE)
@override_waffle_flag(SHOW_REVIEWS_TOOL_FLAG, active=True)
@ddt.data(
diff --git a/openedx/features/course_experience/tests/views/test_course_updates.py b/openedx/features/course_experience/tests/views/test_course_updates.py
index b9113290c1..3a6409f63a 100644
--- a/openedx/features/course_experience/tests/views/test_course_updates.py
+++ b/openedx/features/course_experience/tests/views/test_course_updates.py
@@ -2,25 +2,14 @@
Tests for the course updates page.
"""
-
from datetime import datetime
-import six
from django.urls import reverse
-from lms.djangoapps.courseware.courses import get_course_info_usage_key
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
-from openedx.features.course_experience.views.course_updates import STATUS_VISIBLE
-from common.djangoapps.student.models import CourseEnrollment
-from common.djangoapps.student.tests.factories import UserFactory
-from xmodule.modulestore import ModuleStoreEnum
-from xmodule.modulestore.django import modulestore
-from xmodule.modulestore.exceptions import ItemNotFoundError
-from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
-from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
-
-TEST_PASSWORD = 'test'
+from openedx.features.course_experience.tests import BaseCourseUpdatesTestCase
+from xmodule.modulestore.tests.factories import check_mongo_calls
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
@@ -32,93 +21,18 @@ def course_updates_url(course):
return reverse(
'openedx.course_experience.course_updates',
kwargs={
- 'course_id': six.text_type(course.id),
+ 'course_id': str(course.id),
}
)
-def create_course_update(course, user, content, date='December 31, 1999'):
- """
- Creates a test welcome message for the specified course.
- """
- updates_usage_key = get_course_info_usage_key(course, 'updates')
- try:
- course_updates = modulestore().get_item(updates_usage_key)
- except ItemNotFoundError:
- course_updates = create_course_updates_block(course, user)
- course_updates.items.append({
- "id": len(course_updates.items) + 1,
- "date": date,
- "content": content,
- "status": STATUS_VISIBLE
- })
- modulestore().update_item(course_updates, user.id)
-
-
-def create_course_updates_block(course, user):
- """
- Create a course updates block.
- """
- updates_usage_key = get_course_info_usage_key(course, 'updates')
- course_updates = modulestore().create_item(
- user.id,
- updates_usage_key.course_key,
- updates_usage_key.block_type,
- block_id=updates_usage_key.block_id
- )
- course_updates.data = ''
- return course_updates
-
-
-def remove_course_updates(user, course):
- """
- Remove any course updates in the specified course.
- """
- updates_usage_key = get_course_info_usage_key(course, 'updates')
- try:
- course_updates = modulestore().get_item(updates_usage_key)
- modulestore().delete_item(course_updates.location, user.id)
- except (ItemNotFoundError, ValueError):
- pass
-
-
-class TestCourseUpdatesPage(SharedModuleStoreTestCase):
+class TestCourseUpdatesPage(BaseCourseUpdatesTestCase):
"""
Test the course updates page.
"""
- @classmethod
- def setUpClass(cls):
- """Set up the simplest course possible."""
- # pylint: disable=super-method-not-called
- with super(TestCourseUpdatesPage, cls).setUpClassAndTestData():
- with cls.store.default_store(ModuleStoreEnum.Type.split):
- cls.course = CourseFactory.create()
- with cls.store.bulk_operations(cls.course.id):
- # Create a basic course structure
- chapter = ItemFactory.create(category='chapter', parent_location=cls.course.location)
- section = ItemFactory.create(category='sequential', parent_location=chapter.location)
- ItemFactory.create(category='vertical', parent_location=section.location)
-
- @classmethod
- def setUpTestData(cls):
- """Set up and enroll our fake user in the course."""
- cls.user = UserFactory(password=TEST_PASSWORD)
- CourseEnrollment.enroll(cls.user, cls.course.id)
-
- def setUp(self):
- """
- Set up for the tests.
- """
- super(TestCourseUpdatesPage, self).setUp()
- self.client.login(username=self.user.username, password=TEST_PASSWORD)
-
- def tearDown(self):
- remove_course_updates(self.user, self.course)
- super(TestCourseUpdatesPage, self).tearDown()
-
def test_view(self):
- create_course_update(self.course, self.user, 'First Message')
- create_course_update(self.course, self.user, 'Second Message')
+ self.create_course_update('First Message')
+ self.create_course_update('Second Message')
url = course_updates_url(self.course)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
@@ -127,7 +41,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
def test_queries(self):
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
- create_course_update(self.course, self.user, 'First Message')
+ self.create_course_update('First Message')
# Pre-fetch the view to populate any caches
course_updates_url(self.course)
diff --git a/openedx/features/course_experience/tests/views/test_welcome_message.py b/openedx/features/course_experience/tests/views/test_welcome_message.py
index c75b93b947..805ae431c2 100644
--- a/openedx/features/course_experience/tests/views/test_welcome_message.py
+++ b/openedx/features/course_experience/tests/views/test_welcome_message.py
@@ -2,21 +2,10 @@
Tests for course welcome messages.
"""
-
import ddt
-import six
from django.urls import reverse
-from common.djangoapps.student.models import CourseEnrollment
-from common.djangoapps.student.tests.factories import UserFactory
-from xmodule.modulestore import ModuleStoreEnum
-from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
-from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
-
-from .test_course_updates import create_course_update, remove_course_updates
-
-TEST_PASSWORD = 'test'
-TEST_WELCOME_MESSAGE = '