Commit 5703adb7 by Ben Patterson

Merge branch 'release'

Includes rc/2014-10-14
parents 208c6f1d bb6cd7dc
...@@ -15,10 +15,6 @@ def run(): ...@@ -15,10 +15,6 @@ def run():
""" """
Executed during django startup Executed during django startup
""" """
# Patch the xml libs.
from safe_lxml import defuse_xml_libs
defuse_xml_libs()
django_utils_translation.patch() django_utils_translation.patch()
autostartup() autostartup()
......
# Patch the xml libs before anything else.
from safe_lxml import defuse_xml_libs
defuse_xml_libs()
# Disable PyContract contract checking when running as a webserver # Disable PyContract contract checking when running as a webserver
import contracts import contracts
contracts.disable_all() contracts.disable_all()
......
...@@ -19,6 +19,23 @@ class @DiscussionSpecHelper ...@@ -19,6 +19,23 @@ class @DiscussionSpecHelper
{always: ->} {always: ->}
) )
@makeEventSpy = () ->
jasmine.createSpyObj('event', ['preventDefault', 'target'])
@makeCourseSettings = (is_cohorted=true) ->
new DiscussionCourseSettings(
category_map:
children: ['Test Topic', 'Other Topic']
entries:
'Test Topic':
is_cohorted: is_cohorted
id: 'test_topic'
'Other Topic':
is_cohorted: is_cohorted
id: 'other_topic'
is_cohorted: is_cohorted
)
@setUnderscoreFixtures = -> @setUnderscoreFixtures = ->
for templateName in ['thread-show'] for templateName in ['thread-show']
templateFixture = readFixtures('templates/discussion/' + templateName + '.underscore') templateFixture = readFixtures('templates/discussion/' + templateName + '.underscore')
......
(function() { (function() {
'use strict'; 'use strict';
describe('DiscussionThreadEditView', function() { describe('DiscussionThreadEditView', function() {
var testUpdate, testCancel;
beforeEach(function() { beforeEach(function() {
DiscussionSpecHelper.setUpGlobals(); DiscussionSpecHelper.setUpGlobals();
DiscussionSpecHelper.setUnderscoreFixtures(); DiscussionSpecHelper.setUnderscoreFixtures();
spyOn(DiscussionUtil, 'makeWmdEditor'); spyOn(DiscussionUtil, 'makeWmdEditor');
this.threadData = DiscussionViewSpecHelper.makeThreadWithProps(); this.threadData = DiscussionViewSpecHelper.makeThreadWithProps({
this.thread = new Thread(this.threadData); 'commentable_id': 'test_topic',
this.course_settings = new DiscussionCourseSettings({ 'title': 'test thread title'
'category_map': {
'children': ['Topic'],
'entries': {
'Topic': {
'is_cohorted': true,
'id': 'topic'
}
}
},
'is_cohorted': true
}); });
this.thread = new Thread(this.threadData);
this.course_settings = DiscussionSpecHelper.makeCourseSettings();
this.createEditView = function (options) { this.createEditView = function (options) {
options = _.extend({ options = _.extend({
container: $('#fixture-element'), container: $('#fixture-element'),
model: this.thread, model: this.thread,
mode: 'tab', mode: 'tab',
topicId: 'dummy_id',
threadType: 'question',
course_settings: this.course_settings course_settings: this.course_settings
}, options); }, options);
this.view = new DiscussionThreadEditView(options); this.view = new DiscussionThreadEditView(options);
...@@ -34,36 +26,52 @@ ...@@ -34,36 +26,52 @@
}; };
}); });
it('can save new data correctly', function() { testUpdate = function(view, thread) {
var view;
spyOn($, 'ajax').andCallFake(function(params) { spyOn($, 'ajax').andCallFake(function(params) {
expect(params.url.path()).toEqual(DiscussionUtil.urlFor('update_thread', 'dummy_id')); expect(params.url.path()).toEqual(DiscussionUtil.urlFor('update_thread', 'dummy_id'));
expect(params.data.thread_type).toBe('discussion'); expect(params.data.thread_type).toBe('discussion');
expect(params.data.commentable_id).toBe('topic'); expect(params.data.commentable_id).toBe('other_topic');
expect(params.data.title).toBe('new_title'); expect(params.data.title).toBe('changed thread title');
params.success(); params.success();
return {always: function() {}}; return {always: function() {}};
}); });
this.createEditView(); view.$el.find('a.topic-title')[1].click(); // set new topic
this.view.$el.find('a.topic-title').first().click(); // set new topic view.$('.edit-post-title').val('changed thread title'); // set new title
this.view.$('.edit-post-title').val('new_title'); // set new title view.$("label[for$='post-type-discussion']").click(); // set new thread type
this.view.$("label[for$='post-type-discussion']").click(); // set new thread type view.$('.post-update').click();
this.view.$('.post-update').click();
expect($.ajax).toHaveBeenCalled(); expect($.ajax).toHaveBeenCalled();
expect(this.thread.get('title')).toBe('new_title'); expect(thread.get('title')).toBe('changed thread title');
expect(this.thread.get('commentable_id')).toBe('topic'); expect(thread.get('thread_type')).toBe('discussion');
expect(this.thread.get('thread_type')).toBe('discussion'); expect(thread.get('commentable_id')).toBe('other_topic');
expect(this.thread.get('courseware_title')).toBe('Topic'); expect(thread.get('courseware_title')).toBe('Other Topic');
expect(view.$('.edit-post-title')).toHaveValue('');
expect(view.$('.wmd-preview p')).toHaveText('');
};
it('can save new data correctly in tab mode', function() {
this.createEditView();
testUpdate(this.view, this.thread);
});
expect(this.view.$('.edit-post-title')).toHaveValue(''); it('can save new data correctly in inline mode', function() {
expect(this.view.$('.wmd-preview p')).toHaveText(''); this.createEditView({"mode": "inline"});
testUpdate(this.view, this.thread);
}); });
it('can close the view', function() { testCancel = function(view) {
this.createEditView(); view.$('.post-cancel').click();
this.view.$('.post-cancel').click();
expect($('.edit-post-form')).not.toExist(); expect($('.edit-post-form')).not.toExist();
}
it('can close the view in tab mode', function() {
this.createEditView();
testCancel(this.view);
});
it('can close the view in inline mode', function() {
this.createEditView({"mode": "inline"});
testCancel(this.view);
}); });
}); });
}).call(this); }).call(this);
...@@ -11,6 +11,7 @@ describe "DiscussionThreadView", -> ...@@ -11,6 +11,7 @@ describe "DiscussionThreadView", ->
# Avoid unnecessary boilerplate # Avoid unnecessary boilerplate
spyOn(DiscussionThreadShowView.prototype, "convertMath") spyOn(DiscussionThreadShowView.prototype, "convertMath")
spyOn(DiscussionContentView.prototype, "makeWmdEditor") spyOn(DiscussionContentView.prototype, "makeWmdEditor")
spyOn(DiscussionUtil, "makeWmdEditor")
spyOn(ThreadResponseView.prototype, "renderShowView") spyOn(ThreadResponseView.prototype, "renderShowView")
renderWithContent = (view, content) -> renderWithContent = (view, content) ->
...@@ -46,7 +47,12 @@ describe "DiscussionThreadView", -> ...@@ -46,7 +47,12 @@ describe "DiscussionThreadView", ->
threadData = DiscussionViewSpecHelper.makeThreadWithProps({closed: originallyClosed}) threadData = DiscussionViewSpecHelper.makeThreadWithProps({closed: originallyClosed})
thread = new Thread(threadData) thread = new Thread(threadData)
discussion = new Discussion(thread) discussion = new Discussion(thread)
view = new DiscussionThreadView({ model: thread, el: $("#fixture-element"), mode: mode}) view = new DiscussionThreadView(
model: thread
el: $("#fixture-element")
mode: mode
course_settings: DiscussionSpecHelper.makeCourseSettings()
)
renderWithContent(view, {resp_total: 1, children: [{}]}) renderWithContent(view, {resp_total: 1, children: [{}]})
if mode == "inline" if mode == "inline"
view.expand() view.expand()
...@@ -70,7 +76,12 @@ describe "DiscussionThreadView", -> ...@@ -70,7 +76,12 @@ describe "DiscussionThreadView", ->
describe "tab mode", -> describe "tab mode", ->
beforeEach -> beforeEach ->
@view = new DiscussionThreadView({ model: @thread, el: $("#fixture-element"), mode: "tab"}) @view = new DiscussionThreadView(
model: @thread
el: $("#fixture-element")
mode: "tab"
course_settings: DiscussionSpecHelper.makeCourseSettings()
)
describe "response count and pagination", -> describe "response count and pagination", ->
it "correctly render for a thread with no responses", -> it "correctly render for a thread with no responses", ->
...@@ -111,7 +122,12 @@ describe "DiscussionThreadView", -> ...@@ -111,7 +122,12 @@ describe "DiscussionThreadView", ->
describe "inline mode", -> describe "inline mode", ->
beforeEach -> beforeEach ->
@view = new DiscussionThreadView({ model: @thread, el: $("#fixture-element"), mode: "inline"}) @view = new DiscussionThreadView(
model: @thread
el: $("#fixture-element")
mode: "inline"
course_settings: DiscussionSpecHelper.makeCourseSettings()
)
describe "render", -> describe "render", ->
it "shows content that should be visible when collapsed", -> it "shows content that should be visible when collapsed", ->
...@@ -178,11 +194,26 @@ describe "DiscussionThreadView", -> ...@@ -178,11 +194,26 @@ describe "DiscussionThreadView", ->
expect($(".post-body").text()).toEqual(maliciousAbbreviation) expect($(".post-body").text()).toEqual(maliciousAbbreviation)
expect($(".post-body").html()).not.toContain("<script") expect($(".post-body").html()).not.toContain("<script")
it "re-renders the show view correctly when leaving the edit view", ->
DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []})
@view.render()
@view.expand()
assertExpandedContentVisible(@view, true)
@view.edit()
assertContentVisible(@view, ".edit-post-body", true)
expect(@view.$el.find(".post-actions-list").length).toBe(0)
@view.closeEditView(DiscussionSpecHelper.makeEventSpy())
expect(@view.$el.find(".edit-post-body").length).toBe(0)
assertContentVisible(@view, ".post-actions-list", true)
describe "for question threads", -> describe "for question threads", ->
beforeEach -> beforeEach ->
@thread.set("thread_type", "question") @thread.set("thread_type", "question")
@view = new DiscussionThreadView( @view = new DiscussionThreadView(
{model: @thread, el: $("#fixture-element"), mode: "tab"} model: @thread
el: $("#fixture-element")
mode: "tab"
course_settings: DiscussionSpecHelper.makeCourseSettings()
) )
renderTestCase = (view, numEndorsed, numNonEndorsed) -> renderTestCase = (view, numEndorsed, numNonEndorsed) ->
......
...@@ -127,7 +127,6 @@ describe "NewPostView", -> ...@@ -127,7 +127,6 @@ describe "NewPostView", ->
view.$(".cancel").click() view.$(".cancel").click()
expect(eventSpy).toHaveBeenCalled() expect(eventSpy).toHaveBeenCalled()
expect(view.$(".post-errors").html()).toEqual(""); expect(view.$(".post-errors").html()).toEqual("");
if mode == "tab"
expect($("input[id$='post-type-question']")).toBeChecked() expect($("input[id$='post-type-question']")).toBeChecked()
expect($("input[id$='post-type-discussion']")).not.toBeChecked() expect($("input[id$='post-type-discussion']")).not.toBeChecked()
expect(view.$(".js-post-title").val()).toEqual(""); expect(view.$(".js-post-title").val()).toEqual("");
......
...@@ -17,12 +17,10 @@ describe 'ResponseCommentView', -> ...@@ -17,12 +17,10 @@ describe 'ResponseCommentView', ->
spyOn(DiscussionUtil, "makeWmdEditor") spyOn(DiscussionUtil, "makeWmdEditor")
@view.render() @view.render()
makeEventSpy = () -> jasmine.createSpyObj('event', ['preventDefault', 'target'])
describe '_delete', -> describe '_delete', ->
beforeEach -> beforeEach ->
@comment.updateInfo {ability: {can_delete: true}} @comment.updateInfo {ability: {can_delete: true}}
@event = makeEventSpy() @event = DiscussionSpecHelper.makeEventSpy()
spyOn(@comment, "remove") spyOn(@comment, "remove")
spyOn(@view.$el, "remove") spyOn(@view.$el, "remove")
...@@ -81,9 +79,9 @@ describe 'ResponseCommentView', -> ...@@ -81,9 +79,9 @@ describe 'ResponseCommentView', ->
# Without calling renderEditView first, renderShowView is a no-op # Without calling renderEditView first, renderShowView is a no-op
@view.renderEditView() @view.renderEditView()
@view.renderShowView() @view.renderShowView()
@view.showView.trigger "comment:_delete", makeEventSpy() @view.showView.trigger "comment:_delete", DiscussionSpecHelper.makeEventSpy()
expect(@view._delete).toHaveBeenCalled() expect(@view._delete).toHaveBeenCalled()
@view.showView.trigger "comment:edit", makeEventSpy() @view.showView.trigger "comment:edit", DiscussionSpecHelper.makeEventSpy()
expect(@view.edit).toHaveBeenCalled() expect(@view.edit).toHaveBeenCalled()
expect(@view.$(".edit-post-form#comment_#{@comment.id}")).not.toHaveClass("edit-post-form") expect(@view.$(".edit-post-form#comment_#{@comment.id}")).not.toHaveClass("edit-post-form")
...@@ -92,9 +90,9 @@ describe 'ResponseCommentView', -> ...@@ -92,9 +90,9 @@ describe 'ResponseCommentView', ->
spyOn(@view, "update") spyOn(@view, "update")
spyOn(@view, "cancelEdit") spyOn(@view, "cancelEdit")
@view.renderEditView() @view.renderEditView()
@view.editView.trigger "comment:update", makeEventSpy() @view.editView.trigger "comment:update", DiscussionSpecHelper.makeEventSpy()
expect(@view.update).toHaveBeenCalled() expect(@view.update).toHaveBeenCalled()
@view.editView.trigger "comment:cancel_edit", makeEventSpy() @view.editView.trigger "comment:cancel_edit", DiscussionSpecHelper.makeEventSpy()
expect(@view.cancelEdit).toHaveBeenCalled() expect(@view.cancelEdit).toHaveBeenCalled()
expect(@view.$(".edit-post-form#comment_#{@comment.id}")).toHaveClass("edit-post-form") expect(@view.$(".edit-post-form#comment_#{@comment.id}")).toHaveClass("edit-post-form")
...@@ -138,7 +136,7 @@ describe 'ResponseCommentView', -> ...@@ -138,7 +136,7 @@ describe 'ResponseCommentView', ->
it 'calls the update endpoint correctly and displays the show view on success', -> it 'calls the update endpoint correctly and displays the show view on success', ->
@ajaxSucceed = true @ajaxSucceed = true
@view.update(makeEventSpy()) @view.update(DiscussionSpecHelper.makeEventSpy())
expect($.ajax).toHaveBeenCalled() expect($.ajax).toHaveBeenCalled()
expect($.ajax.mostRecentCall.args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update') expect($.ajax.mostRecentCall.args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update')
expect($.ajax.mostRecentCall.args[0].data.body).toEqual(@updatedBody) expect($.ajax.mostRecentCall.args[0].data.body).toEqual(@updatedBody)
...@@ -148,7 +146,7 @@ describe 'ResponseCommentView', -> ...@@ -148,7 +146,7 @@ describe 'ResponseCommentView', ->
it 'handles AJAX errors', -> it 'handles AJAX errors', ->
originalBody = @comment.get("body") originalBody = @comment.get("body")
@ajaxSucceed = false @ajaxSucceed = false
@view.update(makeEventSpy()) @view.update(DiscussionSpecHelper.makeEventSpy())
expect($.ajax).toHaveBeenCalled() expect($.ajax).toHaveBeenCalled()
expect($.ajax.mostRecentCall.args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update') expect($.ajax.mostRecentCall.args[0].url._parts.path).toEqual('/courses/edX/999/test/discussion/comments/01234567/update')
expect($.ajax.mostRecentCall.args[0].data.body).toEqual(@updatedBody) expect($.ajax.mostRecentCall.args[0].data.body).toEqual(@updatedBody)
......
...@@ -98,14 +98,18 @@ if Backbone? ...@@ -98,14 +98,18 @@ if Backbone?
@$el.append($discussion) @$el.append($discussion)
@newPostForm = $('.new-post-article') @newPostForm = $('.new-post-article')
@threadviews = @discussion.map (thread) -> @threadviews = @discussion.map (thread) =>
new DiscussionThreadView( view = new DiscussionThreadView(
el: @$("article#thread_#{thread.id}"), el: @$("article#thread_#{thread.id}"),
model: thread, model: thread,
mode: "inline", mode: "inline",
course_settings: @course_settings, course_settings: @course_settings,
topicId: discussionId topicId: discussionId
) )
thread.on "thread:thread_type_updated", ->
view.rerender()
view.expand()
return view
_.each @threadviews, (dtv) -> dtv.render() _.each @threadviews, (dtv) -> dtv.render()
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info) DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
@newPostView = new NewPostView( @newPostView = new NewPostView(
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
this.mode = options.mode || 'inline'; this.mode = options.mode || 'inline';
this.course_settings = options.course_settings; this.course_settings = options.course_settings;
this.threadType = this.model.get('thread_type'); this.threadType = this.model.get('thread_type');
this.topicId = options.topicId; this.topicId = this.model.get('commentable_id');
_.bindAll(this); _.bindAll(this);
return this; return this;
}, },
...@@ -28,7 +28,6 @@ ...@@ -28,7 +28,6 @@
this.template = _.template($('#thread-edit-template').html()); this.template = _.template($('#thread-edit-template').html());
this.$el.html(this.template(this.model.toJSON())).appendTo(this.container); this.$el.html(this.template(this.model.toJSON())).appendTo(this.container);
this.submitBtn = this.$('.post-update'); this.submitBtn = this.$('.post-update');
if (this.isTabMode()) {
threadTypeTemplate = _.template($("#thread-type-template").html()); threadTypeTemplate = _.template($("#thread-type-template").html());
this.addField(threadTypeTemplate({form_id: formId})); this.addField(threadTypeTemplate({form_id: formId}));
this.$("#" + formId + "-post-type-" + this.threadType).attr('checked', true); this.$("#" + formId + "-post-type-" + this.threadType).attr('checked', true);
...@@ -37,7 +36,6 @@ ...@@ -37,7 +36,6 @@
course_settings: this.course_settings course_settings: this.course_settings
}); });
this.addField(this.topicView.render()); this.addField(this.topicView.render());
}
DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), 'edit-post-body'); DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), 'edit-post-body');
return this; return this;
}, },
...@@ -55,7 +53,13 @@ ...@@ -55,7 +53,13 @@
var title = this.$('.edit-post-title').val(), var title = this.$('.edit-post-title').val(),
threadType = this.$(".post-type-input:checked").val(), threadType = this.$(".post-type-input:checked").val(),
body = this.$('.edit-post-body textarea').val(), body = this.$('.edit-post-body textarea').val(),
commentableId = this.isTabMode() ? this.topicView.getCurrentTopicId() : null; commentableId = this.topicView.getCurrentTopicId(),
postData = {
title: title,
thread_type: threadType,
body: body,
commentable_id: commentableId
};
return DiscussionUtil.safeAjax({ return DiscussionUtil.safeAjax({
$elem: this.submitBtn, $elem: this.submitBtn,
...@@ -64,32 +68,18 @@ ...@@ -64,32 +68,18 @@
type: 'POST', type: 'POST',
dataType: 'json', dataType: 'json',
async: false, // @TODO when the rest of the stuff below is made to work properly.. async: false, // @TODO when the rest of the stuff below is made to work properly..
data: { data: postData,
title: title,
thread_type: threadType,
body: body,
commentable_id: commentableId
},
error: DiscussionUtil.formErrorHandler(this.$('.post-errors')), error: DiscussionUtil.formErrorHandler(this.$('.post-errors')),
success: function() { success: function() {
var newAttrs = {
title: title,
body: body
};
// @TODO: Move this out of the callback, this makes it feel sluggish // @TODO: Move this out of the callback, this makes it feel sluggish
this.$('.edit-post-title').val('').attr('prev-text', ''); this.$('.edit-post-title').val('').attr('prev-text', '');
this.$('.edit-post-body textarea').val('').attr('prev-text', ''); this.$('.edit-post-body textarea').val('').attr('prev-text', '');
this.$('.wmd-preview p').html(''); this.$('.wmd-preview p').html('');
if (this.isTabMode()) { postData.courseware_title = this.topicView.getFullTopicName();
_.extend(newAttrs, { this.model.set(postData).unset('abbreviatedBody');
thread_type: threadType,
commentable_id: commentableId,
courseware_title: this.topicView.getFullTopicName()
});
}
this.model.set(newAttrs).unset('abbreviatedBody');
this.trigger('thread:updated'); this.trigger('thread:updated');
if (this.threadType !== threadType) { if (this.threadType !== threadType) {
this.model.set("thread_type", threadType)
this.model.trigger('thread:thread_type_updated'); this.model.trigger('thread:thread_type_updated');
this.trigger('comment:endorse'); this.trigger('comment:endorse');
} }
......
...@@ -34,6 +34,20 @@ if Backbone? ...@@ -34,6 +34,20 @@ if Backbone?
if @isQuestion() if @isQuestion()
@markedAnswers = new Comments() @markedAnswers = new Comments()
rerender: () ->
if @showView?
@showView.undelegateEvents()
@undelegateEvents()
@$el.empty()
@initialize(
mode: @mode
model: @model
el: @el
course_settings: @course_settings
topicId: @topicId
)
@render()
renderTemplate: -> renderTemplate: ->
@template = _.template($("#thread-template").html()) @template = _.template($("#thread-template").html())
@template(@model.toJSON()) @template(@model.toJSON())
...@@ -272,7 +286,6 @@ if Backbone? ...@@ -272,7 +286,6 @@ if Backbone?
model: @model model: @model
mode: @mode mode: @mode
course_settings: @options.course_settings course_settings: @options.course_settings
topicId: @model.get('commentable_id')
) )
@editView.bind "thread:updated thread:cancel_edit", @closeEditView @editView.bind "thread:updated thread:cancel_edit", @closeEditView
@editView.bind "comment:endorse", @endorseThread @editView.bind "comment:endorse", @endorseThread
...@@ -296,6 +309,9 @@ if Backbone? ...@@ -296,6 +309,9 @@ if Backbone?
closeEditView: (event) => closeEditView: (event) =>
@createShowView() @createShowView()
@renderShowView() @renderShowView()
# next call is necessary to re-render the post action controls after
# submitting or cancelling a thread edit in inline mode.
@$el.find(".post-extended-content").show()
# If you use "delete" here, it will compile down into JS that includes the # If you use "delete" here, it will compile down into JS that includes the
# use of DiscussionThreadView.prototype.delete, and that will break IE8 # use of DiscussionThreadView.prototype.delete, and that will break IE8
......
...@@ -16,9 +16,9 @@ if Backbone? ...@@ -16,9 +16,9 @@ if Backbone?
form_id: @mode + (if @topicId then "-" + @topicId else "") form_id: @mode + (if @topicId then "-" + @topicId else "")
}) })
@$el.html(_.template($("#new-post-template").html(), context)) @$el.html(_.template($("#new-post-template").html(), context))
if @isTabMode()
threadTypeTemplate = _.template($("#thread-type-template").html()); threadTypeTemplate = _.template($("#thread-type-template").html());
@addField(threadTypeTemplate({form_id: _.uniqueId("form-")})); @addField(threadTypeTemplate({form_id: _.uniqueId("form-")}));
if @isTabMode()
@topicView = new DiscussionTopicMenuView { @topicView = new DiscussionTopicMenuView {
topicId: @topicId topicId: @topicId
course_settings: @course_settings course_settings: @course_settings
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
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