Commit 24d6d377 by Diana Huang Committed by GitHub

Merge pull request #15789 from edx/diana/new-updates-behavior

Add new latest-update fragment and waffle flag.
parents e9028ce8 dcf528a5
...@@ -64,8 +64,8 @@ ...@@ -64,8 +64,8 @@
} }
} }
// Welcome message // Welcome message / Latest Update message
.welcome-message { .welcome-message, .update-message{
border: solid 1px $lms-border-color; border: solid 1px $lms-border-color;
@include border-left(solid 4px $black); @include border-left(solid 4px $black);
margin-bottom: $baseline; margin-bottom: $baseline;
......
...@@ -25,6 +25,12 @@ COURSE_PRE_START_ACCESS_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'pre_star ...@@ -25,6 +25,12 @@ COURSE_PRE_START_ACCESS_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'pre_star
# Waffle flag to enable a review page link from the unified home page # Waffle flag to enable a review page link from the unified home page
SHOW_REVIEWS_TOOL_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_reviews_tool') SHOW_REVIEWS_TOOL_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_reviews_tool')
# Waffle flag to switch between the 'welcome message' and 'latest update' on the course home page.
# Important Admin Note: 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.
LATEST_UPDATE_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'latest_update')
def course_home_page_title(course): # pylint: disable=unused-argument def course_home_page_title(course): # pylint: disable=unused-argument
""" """
......
<div class="update-message">
<h3>Latest Update</h3>
<div class="dismiss-message">
<button type="button" class="btn-link">Dismiss</button>
</div>
This is an update.
</div>
/* globals $ */
import 'jquery.cookie';
export class LatestUpdate { // eslint-disable-line import/prefer-default-export
constructor(options) {
if ($.cookie('update-message') === 'hide') {
$(options.messageContainer).hide();
}
$(options.dismissButton).click(() => {
$.cookie('update-message', 'hide', { expires: 1 });
$(options.messageContainer).hide();
});
}
}
/* globals $, loadFixtures */
import 'jquery.cookie';
import { LatestUpdate } from '../LatestUpdate';
describe('LatestUpdate tests', () => {
function createLatestUpdate() {
new LatestUpdate({ messageContainer: '.update-message', dismissButton: '.dismiss-message button' }); // eslint-disable-line no-new
}
describe('Test dismiss', () => {
beforeEach(() => {
// This causes the cookie to be deleted.
$.cookie('update-message', '', { expires: -1 });
loadFixtures('course_experience/fixtures/latest-update-fragment.html');
});
it('Test dismiss button', () => {
expect($.cookie('update-message')).toBe(null);
createLatestUpdate();
expect($('.update-message').attr('style')).toBe(undefined);
$('.dismiss-message button').click();
expect($('.update-message').attr('style')).toBe('display: none;');
expect($.cookie('update-message')).toBe('hide');
});
it('Test cookie hides update', () => {
$.cookie('update-message', 'hide');
createLatestUpdate();
expect($('.update-message').attr('style')).toBe('display: none;');
$.cookie('update-message', '', { expires: -1 });
loadFixtures('course_experience/fixtures/latest-update-fragment.html');
createLatestUpdate();
expect($('.update-message').attr('style')).toBe(undefined);
});
});
});
...@@ -61,9 +61,9 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV ...@@ -61,9 +61,9 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV
${HTML(course_home_message_fragment.body_html())} ${HTML(course_home_message_fragment.body_html())}
% endif % endif
% if welcome_message_fragment and UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id): % if update_message_fragment and UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):
<div class="section section-dates"> <div class="section section-update-message">
${HTML(welcome_message_fragment.body_html())} ${HTML(update_message_fragment.body_html())}
</div> </div>
% endif % endif
......
## mako
<%page expression_filter="h"/>
<%namespace name='static' file='../static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML
%>
<%block name="content">
<div class="update-message">
<div class="dismiss-message">
<button type="button" class="btn-link">${_("Dismiss")}</button>
</div>
<h3>${_("Latest Update")}</h3>
${HTML(update_html)}
</div>
</%block>
<%static:webpack entry="LatestUpdate">
new LatestUpdate( { messageContainer: '.update-message', dismissButton: '.dismiss-message button'});
</%static:webpack>
...@@ -4,7 +4,7 @@ Tests for the course updates page. ...@@ -4,7 +4,7 @@ Tests for the course updates page.
from courseware.courses import get_course_info_usage_key from courseware.courses import get_course_info_usage_key
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.features.course_experience.views.course_updates import CourseUpdatesFragmentView from openedx.features.course_experience.views.course_updates import STATUS_VISIBLE
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
...@@ -43,7 +43,7 @@ def create_course_update(course, user, content, date='December 31, 1999'): ...@@ -43,7 +43,7 @@ def create_course_update(course, user, content, date='December 31, 1999'):
"id": len(course_updates.items) + 1, "id": len(course_updates.items) + 1,
"date": date, "date": date,
"content": content, "content": content,
"status": CourseUpdatesFragmentView.STATUS_VISIBLE "status": STATUS_VISIBLE
}) })
modulestore().update_item(course_updates, user.id) modulestore().update_item(course_updates, user.id)
......
""" """
Tests for course welcome messages. Tests for course welcome messages.
""" """
import ddt
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from student.models import CourseEnrollment from student.models import CourseEnrollment
...@@ -27,6 +28,18 @@ def welcome_message_url(course): ...@@ -27,6 +28,18 @@ def welcome_message_url(course):
) )
def latest_update_url(course):
"""
Returns the URL for the latest update view.
"""
return reverse(
'openedx.course_experience.latest_update_fragment_view',
kwargs={
'course_id': unicode(course.id),
}
)
def dismiss_message_url(course): def dismiss_message_url(course):
""" """
Returns the URL for the dismiss message endpoint. Returns the URL for the dismiss message endpoint.
...@@ -39,9 +52,12 @@ def dismiss_message_url(course): ...@@ -39,9 +52,12 @@ def dismiss_message_url(course):
) )
@ddt.ddt
class TestWelcomeMessageView(ModuleStoreTestCase): class TestWelcomeMessageView(ModuleStoreTestCase):
""" """
Tests for the course welcome message fragment view. Tests for the course welcome message fragment view.
Also tests the LatestUpdate view because the functionality is similar.
""" """
def setUp(self): def setUp(self):
"""Set up the simplest course possible, then set up and enroll our fake user in the course.""" """Set up the simplest course possible, then set up and enroll our fake user in the course."""
...@@ -61,30 +77,35 @@ class TestWelcomeMessageView(ModuleStoreTestCase): ...@@ -61,30 +77,35 @@ class TestWelcomeMessageView(ModuleStoreTestCase):
remove_course_updates(self.user, self.course) remove_course_updates(self.user, self.course)
super(TestWelcomeMessageView, self).tearDown() super(TestWelcomeMessageView, self).tearDown()
def test_welcome_message(self): @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, '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, 'Second Update', date='January 1, 2017')
create_course_update(self.course, self.user, 'Retroactive Update', date='January 1, 2010') create_course_update(self.course, self.user, 'Retroactive Update', date='January 1, 2010')
response = self.client.get(welcome_message_url(self.course)) response = self.client.get(url_generator(self.course))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Second Update') self.assertContains(response, 'Second Update')
self.assertContains(response, 'Dismiss') self.assertContains(response, 'Dismiss')
def test_replace_urls(self): @ddt.data(welcome_message_url, latest_update_url)
def test_replace_urls(self, url_generator):
img_url = 'img.png' img_url = 'img.png'
create_course_update(self.course, self.user, "<img src='/static/{url}'>".format(url=img_url)) create_course_update(self.course, self.user, "<img src='/static/{url}'>".format(url=img_url))
response = self.client.get(welcome_message_url(self.course)) response = self.client.get(url_generator(self.course))
self.assertContains(response, "/asset-v1:{org}+{course}+{run}+type@asset+block/img.png".format( self.assertContains(response, "/asset-v1:{org}+{course}+{run}+type@asset+block/{url}".format(
org=self.course.id.org, org=self.course.id.org,
course=self.course.id.course, course=self.course.id.course,
run=self.course.id.run run=self.course.id.run,
url=img_url,
)) ))
def test_empty_welcome_message(self): @ddt.data(welcome_message_url, latest_update_url)
response = self.client.get(welcome_message_url(self.course)) def test_empty_message(self, url_generator):
response = self.client.get(url_generator(self.course))
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
def test_dismiss_message(self): 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') create_course_update(self.course, self.user, 'First Update', date='January 1, 2017')
response = self.client.get(welcome_message_url(self.course)) response = self.client.get(welcome_message_url(self.course))
......
...@@ -9,6 +9,7 @@ from views.course_outline import CourseOutlineFragmentView ...@@ -9,6 +9,7 @@ from views.course_outline import CourseOutlineFragmentView
from views.course_reviews import CourseReviewsView from views.course_reviews import CourseReviewsView
from views.course_updates import CourseUpdatesFragmentView, CourseUpdatesView from views.course_updates import CourseUpdatesFragmentView, CourseUpdatesView
from views.course_sock import CourseSockFragmentView from views.course_sock import CourseSockFragmentView
from views.latest_update import LatestUpdateFragmentView
from views.welcome_message import WelcomeMessageFragmentView, dismiss_welcome_message from views.welcome_message import WelcomeMessageFragmentView, dismiss_welcome_message
urlpatterns = [ urlpatterns = [
...@@ -48,6 +49,11 @@ urlpatterns = [ ...@@ -48,6 +49,11 @@ urlpatterns = [
name='openedx.course_experience.welcome_message_fragment_view', name='openedx.course_experience.welcome_message_fragment_view',
), ),
url( url(
r'^latest_update_fragment$',
LatestUpdateFragmentView.as_view(),
name='openedx.course_experience.latest_update_fragment_view',
),
url(
r'course_sock_fragment$', r'course_sock_fragment$',
CourseSockFragmentView.as_view(), CourseSockFragmentView.as_view(),
name='openedx.course_experience.course_sock_fragment_view', name='openedx.course_experience.course_sock_fragment_view',
......
...@@ -24,11 +24,13 @@ from student.models import CourseEnrollment ...@@ -24,11 +24,13 @@ from student.models import CourseEnrollment
from util.views import ensure_valid_course_key from util.views import ensure_valid_course_key
from web_fragments.fragment import Fragment from web_fragments.fragment import Fragment
from .. import LATEST_UPDATE_FLAG
from ..utils import get_course_outline_block_tree from ..utils import get_course_outline_block_tree
from .course_dates import CourseDatesFragmentView from .course_dates import CourseDatesFragmentView
from .course_home_messages import CourseHomeMessageFragmentView from .course_home_messages import CourseHomeMessageFragmentView
from .course_outline import CourseOutlineFragmentView from .course_outline import CourseOutlineFragmentView
from .course_sock import CourseSockFragmentView from .course_sock import CourseSockFragmentView
from .latest_update import LatestUpdateFragmentView
from .welcome_message import WelcomeMessageFragmentView from .welcome_message import WelcomeMessageFragmentView
EMPTY_HANDOUTS_HTML = u'<ol></ol>' EMPTY_HANDOUTS_HTML = u'<ol></ol>'
...@@ -121,9 +123,14 @@ class CourseHomeFragmentView(EdxFragmentView): ...@@ -121,9 +123,14 @@ class CourseHomeFragmentView(EdxFragmentView):
} }
if user_access['is_enrolled'] or user_access['is_staff']: if user_access['is_enrolled'] or user_access['is_staff']:
outline_fragment = CourseOutlineFragmentView().render_to_fragment(request, course_id=course_id, **kwargs) outline_fragment = CourseOutlineFragmentView().render_to_fragment(request, course_id=course_id, **kwargs)
welcome_message_fragment = WelcomeMessageFragmentView().render_to_fragment( if LATEST_UPDATE_FLAG.is_enabled(course_key):
request, course_id=course_id, **kwargs update_message_fragment = LatestUpdateFragmentView().render_to_fragment(
) request, course_id=course_id, **kwargs
)
else:
update_message_fragment = WelcomeMessageFragmentView().render_to_fragment(
request, course_id=course_id, **kwargs
)
course_sock_fragment = CourseSockFragmentView().render_to_fragment(request, course=course, **kwargs) course_sock_fragment = CourseSockFragmentView().render_to_fragment(request, course=course, **kwargs)
has_visited_course, resume_course_url = self._get_resume_course_info(request, course_id) has_visited_course, resume_course_url = self._get_resume_course_info(request, course_id)
else: else:
...@@ -134,7 +141,7 @@ class CourseHomeFragmentView(EdxFragmentView): ...@@ -134,7 +141,7 @@ class CourseHomeFragmentView(EdxFragmentView):
# Set all the fragments # Set all the fragments
outline_fragment = None outline_fragment = None
welcome_message_fragment = None update_message_fragment = None
course_sock_fragment = None course_sock_fragment = None
has_visited_course = None has_visited_course = None
resume_course_url = None resume_course_url = None
...@@ -163,7 +170,7 @@ class CourseHomeFragmentView(EdxFragmentView): ...@@ -163,7 +170,7 @@ class CourseHomeFragmentView(EdxFragmentView):
'resume_course_url': resume_course_url, 'resume_course_url': resume_course_url,
'course_tools': course_tools, 'course_tools': course_tools,
'dates_fragment': dates_fragment, 'dates_fragment': dates_fragment,
'welcome_message_fragment': welcome_message_fragment, 'update_message_fragment': update_message_fragment,
'course_sock_fragment': course_sock_fragment, 'course_sock_fragment': course_sock_fragment,
'disable_courseware_js': True, 'disable_courseware_js': True,
'uses_pattern_library': True, 'uses_pattern_library': True,
......
...@@ -17,6 +17,37 @@ from lms.djangoapps.courseware.views.views import CourseTabView ...@@ -17,6 +17,37 @@ from lms.djangoapps.courseware.views.views import CourseTabView
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from openedx.features.course_experience import default_course_url_name 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, '%B %d, %Y')
except ValueError: # occurs for ill-formatted date values
return datetime.today()
class CourseUpdatesView(CourseTabView): class CourseUpdatesView(CourseTabView):
""" """
...@@ -41,9 +72,6 @@ class CourseUpdatesFragmentView(EdxFragmentView): ...@@ -41,9 +72,6 @@ class CourseUpdatesFragmentView(EdxFragmentView):
""" """
A fragment to render the updates page for a course. A fragment to render the updates page for a course.
""" """
STATUS_VISIBLE = 'visible'
STATUS_DELETED = 'deleted'
def render_to_fragment(self, request, course_id=None, **kwargs): def render_to_fragment(self, request, course_id=None, **kwargs):
""" """
Renders the course's home page as a fragment. Renders the course's home page as a fragment.
...@@ -53,7 +81,7 @@ class CourseUpdatesFragmentView(EdxFragmentView): ...@@ -53,7 +81,7 @@ class CourseUpdatesFragmentView(EdxFragmentView):
course_url_name = default_course_url_name(course.id) course_url_name = default_course_url_name(course.id)
course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)}) course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)})
ordered_updates = self.get_ordered_updates(request, course) ordered_updates = get_ordered_updates(request, course)
plain_html_updates = '' plain_html_updates = ''
if ordered_updates: if ordered_updates:
plain_html_updates = self.get_plain_html_updates(request, course) plain_html_updates = self.get_plain_html_updates(request, course)
...@@ -72,26 +100,8 @@ class CourseUpdatesFragmentView(EdxFragmentView): ...@@ -72,26 +100,8 @@ class CourseUpdatesFragmentView(EdxFragmentView):
return Fragment(html) return Fragment(html)
@classmethod @classmethod
def get_ordered_updates(self, 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') == self.STATUS_VISIBLE]
ordered_updates.sort(
key=lambda item: (self.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
@classmethod
def has_updates(self, request, course): def has_updates(self, request, course):
return len(self.get_ordered_updates(request, course)) > 0 return len(get_ordered_updates(request, course)) > 0
@classmethod @classmethod
def get_plain_html_updates(self, request, course): def get_plain_html_updates(self, request, course):
...@@ -103,13 +113,3 @@ class CourseUpdatesFragmentView(EdxFragmentView): ...@@ -103,13 +113,3 @@ class CourseUpdatesFragmentView(EdxFragmentView):
info_module = get_course_info_section_module(request, request.user, course, 'updates') info_module = get_course_info_section_module(request, request.user, course, 'updates')
info_block = getattr(info_module, '_xmodule', info_module) info_block = getattr(info_module, '_xmodule', info_module)
return info_block.system.replace_urls(info_module.data) if info_module else '' return info_block.system.replace_urls(info_module.data) if info_module else ''
@staticmethod
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()
"""
View logic for handling latest course updates.
Although the welcome message fragment also displays the latest update,
this fragment dismisses the message for a limited time so new updates
will continue to appear, where the welcome message gets permanently
dismissed.
"""
from django.template.loader import render_to_string
from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment
from 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
class LatestUpdateFragmentView(EdxFragmentView):
"""
A fragment that displays the latest course update.
"""
def render_to_fragment(self, request, course_id=None, **kwargs):
"""
Renders the latest update message fragment for the specified course.
Returns: A fragment, or None if there is no latest update message.
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
update_html = self.latest_update_html(request, course)
if not update_html:
return None
context = {
'update_html': update_html,
}
html = render_to_string('course_experience/latest-update-fragment.html', context)
return Fragment(html)
@classmethod
def latest_update_html(cls, request, course):
"""
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
...@@ -9,7 +9,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie ...@@ -9,7 +9,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment from web_fragments.fragment import Fragment
from course_updates import CourseUpdatesFragmentView from course_updates import get_ordered_updates
from courseware.courses import get_course_info_section_module, get_course_with_access from courseware.courses import get_course_info_section_module, get_course_with_access
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from openedx.core.djangoapps.user_api.course_tag.api import set_course_tag, get_course_tag from openedx.core.djangoapps.user_api.course_tag.api import set_course_tag, get_course_tag
...@@ -54,7 +54,7 @@ class WelcomeMessageFragmentView(EdxFragmentView): ...@@ -54,7 +54,7 @@ class WelcomeMessageFragmentView(EdxFragmentView):
Returns the course's welcome message or None if it doesn't have one. Returns the course's welcome message or None if it doesn't have one.
""" """
# Return the course update with the most recent publish date # Return the course update with the most recent publish date
ordered_updates = CourseUpdatesFragmentView.get_ordered_updates(request, course) ordered_updates = get_ordered_updates(request, course)
content = None content = None
if ordered_updates: if ordered_updates:
content = ordered_updates[0]['content'] content = ordered_updates[0]['content']
......
...@@ -22,6 +22,7 @@ var wpconfig = { ...@@ -22,6 +22,7 @@ var wpconfig = {
CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js', CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js',
CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js', CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js',
CourseTalkReviews: './openedx/features/course_experience/static/course_experience/js/CourseTalkReviews.js', CourseTalkReviews: './openedx/features/course_experience/static/course_experience/js/CourseTalkReviews.js',
LatestUpdate: './openedx/features/course_experience/static/course_experience/js/LatestUpdate.js',
WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js', WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js',
Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js', Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js',
Import: './cms/static/js/features/import/factories/import.js', Import: './cms/static/js/features/import/factories/import.js',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment