Commit 550c9858 by Stephen Sanchez

Merge branch 'master' into authoring

Conflicts:
	openassessment/xblock/static/css/openassessment.css
	openassessment/xblock/static/js/openassessment.min.js
parents 5c94f48c 5a115cf1
......@@ -6,7 +6,7 @@ the workflow for a given submission.
"""
import logging
from django.utils import timezone
from django.db import DatabaseError, IntegrityError
from django.db import DatabaseError, IntegrityError, transaction
from dogapi import dog_stats_api
from openassessment.assessment.models import (
......@@ -249,7 +249,7 @@ def create_assessment(
try:
# Retrieve workflow information
scorer_workflow = PeerWorkflow.objects.get(submission_uuid=scorer_submission_uuid)
peer_workflow_item = scorer_workflow.get_latest_open_workflow_item()
peer_workflow_item = scorer_workflow.find_active_assessments()
if peer_workflow_item is None:
message = (
u"There are no open assessments associated with the scorer's "
......@@ -257,27 +257,20 @@ def create_assessment(
).format(scorer_submission_uuid)
logger.warning(message)
raise PeerAssessmentWorkflowError(message)
peer_submission_uuid = peer_workflow_item.author.submission_uuid
peer_submission_uuid = peer_workflow_item.submission_uuid
# Get or create the rubric
rubric = rubric_from_dict(rubric_dict)
# Create the peer assessment
assessment = Assessment.create(
rubric,
assessment = _complete_assessment(
rubric_dict,
scorer_id,
peer_submission_uuid,
PEER_TYPE,
scored_at=scored_at,
feedback=overall_feedback
options_selected,
criterion_feedback,
scorer_workflow,
overall_feedback,
num_required_grades,
scored_at
)
# Create assessment parts for each criterion in the rubric
# This will raise an `InvalidRubricSelection` if the selected options do not match the rubric.
AssessmentPart.create_from_option_names(assessment, options_selected, feedback=criterion_feedback)
# Close the active assessment
scorer_workflow.close_active_assessment(peer_submission_uuid, assessment, num_required_grades)
_log_assessment(assessment, scorer_workflow)
return full_assessment_dict(assessment)
except PeerWorkflow.DoesNotExist:
......@@ -302,6 +295,71 @@ def create_assessment(
logger.exception(error_message)
raise PeerAssessmentInternalError(error_message)
@transaction.commit_on_success
def _complete_assessment(
rubric_dict,
scorer_id,
peer_submission_uuid,
options_selected,
criterion_feedback,
scorer_workflow,
overall_feedback,
num_required_grades,
scored_at
):
"""
Internal function for atomic assessment creation. Creates a peer assessment
and closes the associated peer workflow item in a single transaction.
Args:
rubric_dict (dict): The rubric model associated with this assessment
scorer_id (str): The user ID for the user giving this assessment. This
is required to create an assessment on a submission.
peer_submission_uuid (str): The submission uuid for the submission being
assessed.
options_selected (dict): Dictionary mapping criterion names to the
option names the user selected for that criterion.
criterion_feedback (dict): Dictionary mapping criterion names to the
free-form text feedback the user gave for the criterion.
Since criterion feedback is optional, some criteria may not appear
in the dictionary.
scorer_workflow (PeerWorkflow): The PeerWorkflow associated with the
scorer. Updates the workflow item associated with this assessment.
overall_feedback (unicode): Free-form text feedback on the submission overall.
num_required_grades (int): The required number of assessments a
submission requires before it is completed. If this number of
assessments is reached, the grading_completed_at timestamp is set
for the Workflow.
scored_at (datetime): Optional argument to override the time in which
the assessment took place. If not specified, scored_at is set to
now.
Returns:
The Assessment model
"""
# Get or create the rubric
rubric = rubric_from_dict(rubric_dict)
# Create the peer assessment
assessment = Assessment.create(
rubric,
scorer_id,
peer_submission_uuid,
PEER_TYPE,
scored_at=scored_at,
feedback=overall_feedback
)
# Create assessment parts for each criterion in the rubric
# This will raise an `InvalidRubricSelection` if the selected options do not
# match the rubric.
AssessmentPart.create_from_option_names(assessment, options_selected, feedback=criterion_feedback)
# Close the active assessment
scorer_workflow.close_active_assessment(peer_submission_uuid, assessment, num_required_grades)
return assessment
def get_rubric_max_scores(submission_uuid):
"""Gets the maximum possible value for each criterion option
......@@ -608,7 +666,8 @@ def get_submission_to_assess(submission_uuid, graded_by):
u"A Peer Assessment Workflow does not exist for the student "
u"with submission UUID {}".format(submission_uuid)
)
peer_submission_uuid = workflow.find_active_assessments()
open_item = workflow.find_active_assessments()
peer_submission_uuid = open_item.submission_uuid if open_item else None
# If there is an active assessment for this user, get that submission,
# otherwise, get the first assessment for review, otherwise,
# get the first submission available for over grading ("over-grading").
......
......@@ -2,7 +2,7 @@
Public interface for self-assessment.
"""
import logging
from django.db import DatabaseError
from django.db import DatabaseError, transaction
from dogapi import dog_stats_api
from submissions.api import get_submission_and_student, SubmissionNotFoundError
......@@ -152,21 +152,15 @@ def create_assessment(
raise SelfAssessmentRequestError()
try:
# Get or create the rubric
rubric = rubric_from_dict(rubric_dict)
# Create the self assessment
assessment = Assessment.create(
rubric,
user_id,
assessment = _complete_assessment(
submission_uuid,
SELF_TYPE,
scored_at=scored_at,
feedback=overall_feedback
user_id,
options_selected,
criterion_feedback,
overall_feedback,
rubric_dict,
scored_at
)
# This will raise an `InvalidRubricSelection` if the selected options do not match the rubric.
AssessmentPart.create_from_option_names(assessment, options_selected, feedback=criterion_feedback)
_log_assessment(assessment, submission)
except InvalidRubric as ex:
msg = "Invalid rubric definition: " + str(ex)
......@@ -181,6 +175,56 @@ def create_assessment(
return full_assessment_dict(assessment)
@transaction.commit_on_success
def _complete_assessment(
submission_uuid,
user_id,
options_selected,
criterion_feedback,
overall_feedback,
rubric_dict,
scored_at
):
"""
Internal function for creating an assessment and its parts atomically.
Args:
submission_uuid (str): The unique identifier for the submission being
assessed.
user_id (str): The ID of the user creating the assessment. This must
match the ID of the user who made the submission.
options_selected (dict): Mapping of rubric criterion names to option
values selected.
criterion_feedback (dict): Dictionary mapping criterion names to the
free-form text feedback the user gave for the criterion.
Since criterion feedback is optional, some criteria may not appear
in the dictionary.
overall_feedback (unicode): Free-form text feedback on the submission overall.
rubric_dict (dict): Serialized Rubric model.
scored_at (datetime): The timestamp of the assessment.
Returns:
Assessment model
"""
# Get or create the rubric
rubric = rubric_from_dict(rubric_dict)
# Create the self assessment
assessment = Assessment.create(
rubric,
user_id,
submission_uuid,
SELF_TYPE,
scored_at=scored_at,
feedback=overall_feedback
)
# This will raise an `InvalidRubricSelection` if the selected options do not match the rubric.
AssessmentPart.create_from_option_names(assessment, options_selected, feedback=criterion_feedback)
return assessment
def get_assessment(submission_uuid):
"""
Retrieve a self-assessment for a submission_uuid.
......
......@@ -216,13 +216,29 @@ class PeerWorkflow(models.Model):
for this PeerWorkflow.
Returns:
submission_uuid (str): The submission_uuid for the submission that the
(PeerWorkflowItem) The PeerWorkflowItem for the submission that the
student has open for active assessment.
"""
oldest_acceptable = now() - self.TIME_LIMIT
workflows = self.graded.filter(assessment__isnull=True, started_at__gt=oldest_acceptable) # pylint:disable=E1101
return workflows[0].submission_uuid if workflows else None
items = list(self.graded.all().order_by("-started_at", "-id"))
valid_open_items = []
completed_sub_uuids = []
# First, remove all completed items.
for item in items:
if item.assessment is not None:
completed_sub_uuids.append(item.submission_uuid)
else:
valid_open_items.append(item)
# Remove any open items which have a submission which has been completed.
for item in valid_open_items:
if (item.started_at < oldest_acceptable or
item.submission_uuid in completed_sub_uuids):
valid_open_items.remove(item)
return valid_open_items[0] if valid_open_items else None
def get_submission_for_review(self, graded_by):
"""
......@@ -331,19 +347,6 @@ class PeerWorkflow(models.Model):
logger.exception(error_message)
raise PeerAssessmentInternalError(error_message)
def get_latest_open_workflow_item(self):
"""
Return the latest open workflow item for this workflow.
Returns:
A PeerWorkflowItem that is open for assessment.
None if no item is found.
"""
workflow_query = self.graded.filter(assessment__isnull=True).order_by("-started_at", "-id") # pylint:disable=E1101
items = list(workflow_query[:1])
return items[0] if items else None
def close_active_assessment(self, submission_uuid, assessment, num_required_grades):
"""
Updates a workflow item on the student's workflow with the associated
......
......@@ -151,7 +151,7 @@ class TestPeerApi(CacheResetTest):
Tests for the peer assessment API functions.
"""
CREATE_ASSESSMENT_NUM_QUERIES = 59
CREATE_ASSESSMENT_NUM_QUERIES = 58
def test_create_assessment_points(self):
self._create_student_and_submission("Tim", "Tim's answer")
......@@ -879,8 +879,8 @@ class TestPeerApi(CacheResetTest):
PeerWorkflow.create_item(buffy_workflow, xander_answer["uuid"])
# Check to see if Buffy is still actively reviewing Xander's submission.
submission_uuid = buffy_workflow.find_active_assessments()
self.assertEqual(xander_answer["uuid"], submission_uuid)
item = buffy_workflow.find_active_assessments()
self.assertEqual(xander_answer["uuid"], item.submission_uuid)
def test_get_workflow_by_uuid(self):
buffy_answer, _ = self._create_student_and_submission("Buffy", "Buffy's answer")
......@@ -1212,6 +1212,70 @@ class TestPeerApi(CacheResetTest):
MONDAY,
)
def test_ignore_duplicate_workflow_items(self):
"""
A race condition may cause two workflow items to be opened for a single
submission. In this case, we want to be defensive in the API, such that
no open workflow item is acknowledged if an assessment has already been
made against the associated submission.
"""
bob_sub, bob = self._create_student_and_submission('Bob', 'Bob submission')
tim_sub, tim = self._create_student_and_submission('Tim', 'Tim submission')
sally_sub, sally = self._create_student_and_submission('Sally', 'Sally submission')
jane_sub, jane = self._create_student_and_submission('Jane', 'Jane submission')
# Create two workflow items.
peer_api.create_peer_workflow_item(bob_sub['uuid'], tim_sub['uuid'])
peer_api.create_peer_workflow_item(bob_sub['uuid'], tim_sub['uuid'])
# Assess the submission, then get the next submission.
peer_api.create_assessment(
bob_sub['uuid'],
bob['student_id'],
ASSESSMENT_DICT['options_selected'],
ASSESSMENT_DICT['criterion_feedback'],
ASSESSMENT_DICT['overall_feedback'],
RUBRIC_DICT,
REQUIRED_GRADED_BY,
MONDAY
)
# Verify the next submission is not Tim again, but Sally.
next_sub = peer_api.get_submission_to_assess(bob_sub['uuid'], REQUIRED_GRADED_BY)
self.assertEqual(next_sub['uuid'], sally_sub['uuid'])
# Request another peer submission. Should pick up Sally again.
next_sub = peer_api.get_submission_to_assess(bob_sub['uuid'], REQUIRED_GRADED_BY)
self.assertEqual(next_sub['uuid'], sally_sub['uuid'])
# Ensure that the next assessment made is against Sally, not Tim.
# Assess the submission, then get the next submission.
peer_api.create_assessment(
bob_sub['uuid'],
bob['student_id'],
ASSESSMENT_DICT['options_selected'],
ASSESSMENT_DICT['criterion_feedback'],
ASSESSMENT_DICT['overall_feedback'],
RUBRIC_DICT,
REQUIRED_GRADED_BY,
MONDAY
)
# Make sure Tim has one assessment.
tim_assessments = peer_api.get_assessments(tim_sub['uuid'], scored_only=False)
self.assertEqual(1, len(tim_assessments))
# Make sure Sally has one assessment.
sally_assessments = peer_api.get_assessments(sally_sub['uuid'], scored_only=False)
self.assertEqual(1, len(sally_assessments))
# Make sure Jane has no assessment.
jane_assessments = peer_api.get_assessments(jane_sub['uuid'], scored_only=False)
self.assertEqual(0, len(jane_assessments))
def test_get_submission_to_assess_no_workflow(self):
# Try to retrieve a submission to assess when the student
# doing the assessment hasn't yet submitted.
......
......@@ -8,7 +8,11 @@
<span class="step__label">{% trans "Your Grade" %}: </span>
<span class="grade__value">
<span class="grade__value__title">
{% blocktrans with points_earned=score.points_earned points_possible=score.points_possible%}<span class="grade__value__earned">{{ points_earned }}</span> out of <span class="grade__value__potential">{{ points_possible }}</span>{% endblocktrans %}
{% with points_earned_string=score.points_earned|stringformat:"s" points_possible_string=score.points_possible|stringformat:"s" %}
{% blocktrans with points_earned='<span class="grade__value__earned">'|safe|add:points_earned_string|add:'</span>'|safe points_possible='<span class="grade__value__potential">'|safe|add:points_possible_string|add:'</span>'|safe %}
{{ points_earned }} out of {{ points_possible }}
{% endblocktrans %}
{% endwith %}
</span>
</span>
{% else %}
......
......@@ -22,11 +22,11 @@
{% trans "All submitted peer responses have been assessed. Check back later to see if more students have submitted responses. " %}
{% endif %}
{% if has_self %}
{% blocktrans with peer_start_tag = '<a data-behavior="ui-scroll" href="#openassessment__peer-assessment">'|safe self_start_tag = '<a data-behavior="ui-scroll" href="#openassessment__self-assessment">'|safe end_tag = '</a>'|safe %}
{% blocktrans with peer_start_tag='<a data-behavior="ui-scroll" href="#openassessment__peer-assessment">'|safe self_start_tag='<a data-behavior="ui-scroll" href="#openassessment__self-assessment">'|safe end_tag='</a>'|safe %}
You'll receive your grade after you complete the {{ peer_start_tag }}peer assessment{{ end_tag }} and {{ self_start_tag }}self assessment{{ end_tag }} steps, and after your peers have assessed your response.
{% endblocktrans %}
{% else %}
{% blocktrans with start_tag = '<a data-behavior="ui-scroll" href="#openassessment__peer-assessment">'|safe end_tag = '</a>'|safe %}
{% blocktrans with start_tag='<a data-behavior="ui-scroll" href="#openassessment__peer-assessment">'|safe end_tag='</a>'|safe %}
You'll receive your grade after you complete the {{ start_tag }}peer assessment{{ end_tag }} step.
{% endblocktrans %}
{% endif %}
......
......@@ -19,11 +19,11 @@
<strong> {% trans "Self evaluation of this assignment will close soon. " %} </strong>
{% endif %}
{% if has_peer %}
{% blocktrans with start_tag = '<a data-behavior="ui-scroll" href="#openassessment__self-assessment">'|safe end_tag = '</a>'|safe %}
{% blocktrans with start_tag='<a data-behavior="ui-scroll" href="#openassessment__self-assessment">'|safe end_tag='</a>'|safe %}
You'll receive your grade after the required number of your peers have assessed your response and you complete the {{ start_tag }}self assessment{{ end_tag }} step.
{% endblocktrans %}
{% else %}
{% blocktrans with start_tag = '<a data-behavior="ui-scroll" href="#openassessment__self-assessment">'|safe end_tag = '</a>'|safe %}
{% blocktrans with start_tag='<a data-behavior="ui-scroll" href="#openassessment__self-assessment">'|safe end_tag='</a>'|safe %}
You'll receive your grade after you complete the {{ start_tag }}self assessment{{ end_tag}} step.
{% endblocktrans %}
{% endif %}
......
......@@ -28,13 +28,13 @@
<span class="step__status">
<span class="step__status__label">{% trans "This step's status" %}:</span>
<span class="step__status__value">
{% with graded=graded must_grade=must_grade %}
<span class="copy">
{% blocktrans with num_graded="<span class=\"step__status__value--completed\">"|add:graded|add:"</span>"|safe num_must_grade="<span class=\"step__status__value--required\">"|add:must_grade|add:"</span>"|safe %}
{% with graded_string=graded|stringformat:"s" must_grade_string=must_grade|stringformat:"s" %}
{% blocktrans with num_graded='<span class="step__status__value--completed">'|safe|add:graded_string|add:"</span>"|safe num_must_grade='<span class="step__status__value--required">'|safe|add:must_grade_string|add:"</span>"|safe %}
In Progress ({{ num_graded }} of {{ num_must_grade }})
{% endblocktrans %}
{% endwith %}
</span>
{% endwith %}
</span>
</span>
{% endblock %}
......@@ -53,13 +53,13 @@
<article class="peer-assessment" id="peer-assessment--001">
<div class="peer-assessment__display">
<header class="peer-assessment__display__header">
{% with review_num=review_num must_grade=must_grade %}
<h3 class="peer-assessment__display__title">
{% blocktrans with review_number='<span>'|add:review_num|add:'</span>'|safe num_must_grade='<span class="peer-assessment__number--required">'|add:must_grade|add:'</span>'|safe %}
{% with review_num_string=review_num|stringformat:"s" must_grade_string=must_grade|stringformat:"s" %}
{% blocktrans with review_number='<span class="peer-assessment__number--current">'|safe|add:review_num_string|add:'</span>'|safe num_must_grade='<span class="peer-assessment__number--required">'|safe|add:must_grade_string|add:'</span>'|safe %}
Assessment # {{ review_number }} of {{ num_must_grade }}
{% endblocktrans %}
{% endwith %}
</h3>
{% endwith %}
</header>
<div class="peer-assessment__display__response">
......
......@@ -11,9 +11,11 @@
<span class="step__status__value">
<span class="copy">
<i class="ico icon-warning-sign"></i>
{% blocktrans with num_graded="<span class=\"step__status__value--completed\">"|add:graded|add:"</span>"|safe num_must_grade="<span class=\"step__status__value--required\">"|add:must_grade|add:"</span>"|safe %}
Incomplete ({{ num_graded }} of {{ num_must_grade }})
{% endblocktrans %}
{% with graded_string=graded|stringformat:"s" must_grade_string=must_grade|stringformat:"s" %}
{% blocktrans with num_graded='<span class="step__status__value--completed">'|safe|add:graded_string|add:"</span>"|safe num_must_grade='<span class="step__status__value--required">'|safe|add:must_grade_string|add:"</span>"|safe %}
Incomplete ({{ num_graded }} of {{ num_must_grade }})
{% endblocktrans %}
{% endwith %}
</span>
</span>
</span>
......
......@@ -11,9 +11,11 @@
<span class="step__status__value">
<i class="ico icon-ok"></i>
<span class="copy">
{% blocktrans with num_graded='<span class="step__status__value--completed">'|add:graded|add:'</span>'|safe %}
{% with graded_string=graded|stringformat:"s" %}
{% blocktrans with num_graded='<span class="step__status__value--completed">'|safe|add:graded_string|add:'</span>'|safe %}
Complete ({{ num_graded }})
{% endblocktrans %}
{% endwith %}
</span>
</span>
</span>
......
......@@ -11,9 +11,11 @@
<span class="step__status__value">
<i class="ico icon-ok"></i>
<span class="copy">
{% blocktrans with num_graded='<span class="step__status__value--completed">'|add:graded|add:'</span>'|safe %}
{% with graded_string=graded|stringformat:"s" %}
{% blocktrans with num_graded='<span class="step__status__value--completed">'|safe|add:graded_string|add:'</span>'|safe %}
Complete ({{ num_graded }})
{% endblocktrans %}
{% endwith %}
</span>
</span>
</span>
......
......@@ -10,9 +10,11 @@
<span class="step__status__label">{% trans "This step's status" %}:</span>
<span class="step__status__value">
<span class="copy">
{% blocktrans with num_graded="<span class=\"step__status__value--completed\">"|add:graded|add:"</span>"|safe num_must_grade="<span class=\"step__status__value--required\">"|add:must_grade|add:"</span>"|safe %}
In Progress ({{ num_graded }} of {{ num_must_grade }})
{% endblocktrans %}
{% with graded_string=graded|stringformat:"s" must_grade_string=must_grade|stringformat:"s" %}
{% blocktrans with num_graded='<span class="step__status__value--completed">'|safe|add:graded_string|add:"</span>"|safe num_must_grade='<span class="step__status__value--required">'|safe|add:must_grade_string|add:"</span>"|safe %}
In Progress ({{ num_graded }} of {{ num_must_grade }})
{% endblocktrans %}
{% endwith %}
</span>
</span>
</span>
......
......@@ -46,7 +46,7 @@
<h3 class="message__title">{% trans "Learning to Assess Responses" %}</h3>
<div class="message__content">
<p>{% blocktrans %}Before you begin to assess your peers' responses, you'll learn how to complete peer assessments by reviewing responses that instructors have already assessed. If you select the same options for the response that the instructor selected, you'll move to the next step. If you don't select the same options, you'll review the response and try again.{% endblocktrans %}</p>
<p>{% trans "Before you begin to assess your peers' responses, you'll learn how to complete peer assessments by reviewing responses that instructors have already assessed. If you select the same options for the response that the instructor selected, you'll move to the next step. If you don't select the same options, you'll review the response and try again." %}</p>
</div>
</div>
......@@ -54,7 +54,7 @@
<h3 class="message__title">{% trans "Learning to Assess Responses" %}</h3>
<div class="message__content">
<p>{% blocktrans %}Your assessment differs from the instructor's assessment of this response. Review the response and consider why the instructor may have assessed it differently. Then, try the assessment again.{% endblocktrans %}</p>
<p>{% trans "Your assessment differs from the instructor's assessment of this response. Review the response and consider why the instructor may have assessed it differently. Then, try the assessment again." %}</p>
</div>
</div>
......@@ -63,9 +63,11 @@
<header class="student-training__display__header">
{% with training_num_current=training_num_current training_num_available=training_num_available %}
<h3 class="student-training__display__title">
{% blocktrans with current_progress_num='<span class="student-training__number--current">'|add:training_num_current|add:'</span>'|safe num_to_complete='<span class="student-training__number--required">'|add:training_num_available|add:'</span>'|safe %}
{% with training_num_current_string=training_num_current|stringformat:"s" training_num_available_string=training_num_available|stringformat:"s" %}
{% blocktrans with current_progress_num='<span class="student-training__number--current">'|safe|add:training_num_current_string|add:'</span>'|safe num_to_complete='<span class="student-training__number--required">'|safe|add:training_num_available_string|add:'</span>'|safe %}
Training Assessment # {{ current_progress_num }} of {{ num_to_complete }}
{% endblocktrans %}
{% endwith %}
</h3>
{% endwith %}
</header>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -26,6 +26,16 @@ describe("OpenAssessment.BaseView", function() {
defer.resolveWith(this, [server.fragments[component]]);
}).promise();
};
var successPromise = $.Deferred(
function(defer) {
defer.resolve();
}
).promise();
this.peerAssess = function(optionsSelected, feedback) {
return successPromise;
};
};
// Stub runtime
......@@ -75,4 +85,20 @@ describe("OpenAssessment.BaseView", function() {
expect(server.fragmentsLoaded).toContain("grade");
});
});
it("Only load the peer section once on submit", function() {
loadSubviews(function() {
// Simulate a server error
view.peerView.peerAssess();
var numPeerLoads = 0;
for (var i = 0; i < server.fragmentsLoaded.length; i++) {
if (server.fragmentsLoaded[i] == 'peer_assessment') {
numPeerLoads++;
}
}
// Peer should be called twice, once when loading the views,
// and again after the peer has been assessed.
expect(numPeerLoads).toBe(2);
});
});
});
......@@ -158,7 +158,6 @@ OpenAssessment.PeerView.prototype = {
var view = this;
var baseView = view.baseView;
this.peerAssessRequest(function() {
view.load();
baseView.loadAssessmentModules();
baseView.scrollToTop();
});
......
......@@ -1086,10 +1086,15 @@
display: none;
}
.wrapper--copy{
margin-left: 0;
padding-left: 0;
border-left: 0;
.step__title {
width: 100%;
.wrapper--copy{
margin-left: 0;
padding-left: 0;
border-left: 0;
width: 100%;
}
}
@include media($bp-m) {
......
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