diff --git a/lms/djangoapps/course_home_api/outline/v1/views.py b/lms/djangoapps/course_home_api/outline/v1/views.py
index f79730a01e46d1512f51e79281f7f3a892d22fec..b2a64703c129964304f0ff1f22d9dc58a868c9cd 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 bc8eb3140cd68b3862f0da6745387e0af4aaa62c..5599fbc6191beb24b00a3e5ec7f42b1e8dee80ff 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 0000000000000000000000000000000000000000..37de393ca1a8aea84c55f3a711177d88d133b046
--- /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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7eb664cf7a6c986a1589221f820d3c3ccd9954fa 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 0000000000000000000000000000000000000000..7646788152252e509563b07e35e4e262ec4f34d6
--- /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 = '<em>HTML Content</em>'
+        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("<img src='/static/img.png'>")
+        updates = get_ordered_updates(self.request, self.course)
+        expected = "<img src='/asset-v1:{org}+{course}+{run}+type@asset+block/img.png'>".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 5c727ac54ae43297faca23f088248e8b6ca432a1..389d2f7e90d90bf18decc359a3e701ec1725d7b6 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 b9113290c1a7b6201abf57e8551040804a36f9d1..3a6409f63a09d6f1d04510df1708715fa9c8fda7 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 c75b93b947c3fd8562b4e3105ea13955089019ca..805ae431c230335ef5e1d74ca7507917e973c339 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 = '<h2>Welcome!</h2>'
+from openedx.features.course_experience.tests import BaseCourseUpdatesTestCase
 
 
 def welcome_message_url(course):
@@ -26,7 +15,7 @@ def welcome_message_url(course):
     return reverse(
         'openedx.course_experience.welcome_message_fragment_view',
         kwargs={
-            'course_id': six.text_type(course.id),
+            'course_id': str(course.id),
         }
     )
 
@@ -38,7 +27,7 @@ def latest_update_url(course):
     return reverse(
         'openedx.course_experience.latest_update_fragment_view',
         kwargs={
-            'course_id': six.text_type(course.id),
+            'course_id': str(course.id),
         }
     )
 
@@ -50,58 +39,28 @@ def dismiss_message_url(course):
     return reverse(
         'openedx.course_experience.dismiss_welcome_message',
         kwargs={
-            'course_id': six.text_type(course.id),
+            'course_id': str(course.id),
         }
     )
 
 
 @ddt.ddt
-class TestWelcomeMessageView(ModuleStoreTestCase):
+class TestWelcomeMessageView(BaseCourseUpdatesTestCase):
     """
     Tests for the course welcome message fragment view.
 
     Also tests the LatestUpdate view because the functionality is similar.
     """
-    def setUp(self):
-        """Set up the simplest course possible, then set up and enroll our fake user in the course."""
-        super(TestWelcomeMessageView, self).setUp()
-        with self.store.default_store(ModuleStoreEnum.Type.split):
-            self.course = CourseFactory.create()
-            with self.store.bulk_operations(self.course.id):
-                # Create a basic course structure
-                chapter = ItemFactory.create(category='chapter', parent_location=self.course.location)
-                section = ItemFactory.create(category='sequential', parent_location=chapter.location)
-                ItemFactory.create(category='vertical', parent_location=section.location)
-        self.user = UserFactory(password=TEST_PASSWORD)
-        CourseEnrollment.enroll(self.user, self.course.id)
-        self.client.login(username=self.user.username, password=TEST_PASSWORD)
-
-    def tearDown(self):
-        remove_course_updates(self.user, self.course)
-        super(TestWelcomeMessageView, self).tearDown()
-
     @ddt.data(welcome_message_url, latest_update_url)
     def test_message_display(self, url_generator):
-        create_course_update(self.course, self.user, 'First Update', date='January 1, 2000')
-        create_course_update(self.course, self.user, 'Second Update', date='January 1, 2017')
-        create_course_update(self.course, self.user, 'Retroactive Update', date='January 1, 2010')
+        self.create_course_update('First Update', date='January 1, 2000')
+        self.create_course_update('Second Update', date='January 1, 2017')
+        self.create_course_update('Retroactive Update', date='January 1, 2010')
         response = self.client.get(url_generator(self.course))
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, 'Second Update')
         self.assertContains(response, 'Dismiss')
 
-    @ddt.data(welcome_message_url, latest_update_url)
-    def test_replace_urls(self, url_generator):
-        img_url = 'img.png'
-        create_course_update(self.course, self.user, u"<img src='/static/{url}'>".format(url=img_url))
-        response = self.client.get(url_generator(self.course))
-        self.assertContains(response, "/asset-v1:{org}+{course}+{run}+type@asset+block/{url}".format(
-            org=self.course.id.org,
-            course=self.course.id.course,
-            run=self.course.id.run,
-            url=img_url,
-        ))
-
     @ddt.data(welcome_message_url, latest_update_url)
     def test_empty_message(self, url_generator):
         response = self.client.get(url_generator(self.course))
@@ -109,7 +68,7 @@ class TestWelcomeMessageView(ModuleStoreTestCase):
 
     def test_dismiss_welcome_message(self):
         # Latest update is dimssed in JS and has no server/backend component.
-        create_course_update(self.course, self.user, 'First Update', date='January 1, 2017')
+        self.create_course_update('First Update')
 
         response = self.client.get(welcome_message_url(self.course))
         self.assertEqual(response.status_code, 200)
diff --git a/openedx/features/course_experience/views/course_updates.py b/openedx/features/course_experience/views/course_updates.py
index 726b52d1022187c4ed9cb1a5017ee961a3674139..e6c78c4e17ff651961b32733edc669b6ad09a7db 100644
--- a/openedx/features/course_experience/views/course_updates.py
+++ b/openedx/features/course_experience/views/course_updates.py
@@ -2,9 +2,6 @@
 Views that handle course updates.
 """
 
-
-from datetime import datetime
-
 import six
 from django.contrib.auth.decorators import login_required
 from django.template.context_processors import csrf
@@ -19,37 +16,7 @@ from lms.djangoapps.courseware.courses import get_course_info_section_module, ge
 from lms.djangoapps.courseware.views.views import CourseTabView
 from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
 from openedx.features.course_experience import default_course_url_name
-
-STATUS_VISIBLE = 'visible'
-STATUS_DELETED = 'deleted'
-
-
-def get_ordered_updates(request, course):
-    """
-    Returns any course updates in reverse chronological order.
-    """
-    info_module = get_course_info_section_module(request, request.user, course, 'updates')
-
-    updates = info_module.items if info_module else []
-    info_block = getattr(info_module, '_xmodule', info_module) if info_module else None
-    ordered_updates = [update for update in updates 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 safe_parse_date(date):
-    """
-    Since this is used solely for ordering purposes, use today's date as a default
-    """
-    try:
-        return datetime.strptime(date, u'%B %d, %Y')
-    except ValueError:  # occurs for ill-formatted date values
-        return datetime.today()
+from openedx.features.course_experience.course_updates import get_ordered_updates
 
 
 class CourseUpdatesView(CourseTabView):
diff --git a/openedx/features/course_experience/views/latest_update.py b/openedx/features/course_experience/views/latest_update.py
index 9b1b9df9350708130de425580c21f28722749d36..0fe5c3ce727ab3fcdb5ea1211a47dd330475f2a8 100644
--- a/openedx/features/course_experience/views/latest_update.py
+++ b/openedx/features/course_experience/views/latest_update.py
@@ -14,7 +14,7 @@ from web_fragments.fragment import Fragment
 
 from lms.djangoapps.courseware.courses import get_course_with_access
 from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
-from openedx.features.course_experience.views.course_updates import get_ordered_updates
+from openedx.features.course_experience.course_updates import get_current_update_for_user
 
 
 class LatestUpdateFragmentView(EdxFragmentView):
@@ -47,9 +47,4 @@ class LatestUpdateFragmentView(EdxFragmentView):
         Returns the course's latest update message or None if it doesn't have one.
         """
         # Return the course update with the most recent publish date
-        ordered_updates = get_ordered_updates(request, course)
-        content = None
-        if ordered_updates:
-            content = ordered_updates[0]['content']
-
-        return content
+        return get_current_update_for_user(request, course)
diff --git a/openedx/features/course_experience/views/welcome_message.py b/openedx/features/course_experience/views/welcome_message.py
index dfb2dc5e86061e360f8d946c252ffebf0d6a0aa8..aad304201ca921dc325f7947edddfd07ef13478c 100644
--- a/openedx/features/course_experience/views/welcome_message.py
+++ b/openedx/features/course_experience/views/welcome_message.py
@@ -13,11 +13,9 @@ from web_fragments.fragment import Fragment
 
 from lms.djangoapps.courseware.courses import get_course_with_access
 from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
-from openedx.core.djangoapps.user_api.course_tag.api import get_course_tag, set_course_tag
-
-from .course_updates import get_ordered_updates
-
-PREFERENCE_KEY = 'view-welcome-message'
+from openedx.features.course_experience.course_updates import (
+    dismiss_current_update_for_user, get_current_update_for_user,
+)
 
 
 class WelcomeMessageFragmentView(EdxFragmentView):
@@ -46,11 +44,8 @@ class WelcomeMessageFragmentView(EdxFragmentView):
             'welcome_message_html': welcome_message_html,
         }
 
-        if get_course_tag(request.user, course_key, PREFERENCE_KEY) == 'False':
-            return None
-        else:
-            html = render_to_string('course_experience/welcome-message-fragment.html', context)
-            return Fragment(html)
+        html = render_to_string('course_experience/welcome-message-fragment.html', context)
+        return Fragment(html)
 
     @classmethod
     def welcome_message_html(cls, request, course):
@@ -58,12 +53,7 @@ class WelcomeMessageFragmentView(EdxFragmentView):
         Returns the course's welcome message or None if it doesn't have one.
         """
         # Return the course update with the most recent publish date
-        ordered_updates = get_ordered_updates(request, course)
-        content = None
-        if ordered_updates:
-            content = ordered_updates[0]['content']
-
-        return content
+        return get_current_update_for_user(request, course)
 
 
 @ensure_csrf_cookie
@@ -72,5 +62,6 @@ def dismiss_welcome_message(request, course_id):
     Given the course_id in the request, disable displaying the welcome message for the user.
     """
     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 HttpResponse()