Commit f66f74e4 by Awais Committed by Awais Qureshi

Add accept all button on course detail page. User can accept the other's changes.

ECOM-7816
parent 08624046
...@@ -925,3 +925,56 @@ class CoursesAutoCompleteTests(TestCase): ...@@ -925,3 +925,56 @@ class CoursesAutoCompleteTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode('utf-8')) data = json.loads(response.content.decode('utf-8'))
self.assertEqual(len(data['results']), expected_length) self.assertEqual(len(data['results']), expected_length)
class AcceptAllByRevisionTests(TestCase):
def setUp(self):
super(AcceptAllByRevisionTests, self).setUp()
self.user = UserFactory()
self.client.login(username=self.user.username, password=USER_PASSWORD)
self.course = factories.CourseFactory(title='first title', changed_by=self.user)
# update title so that another revision created
self.course.title = "updated title"
self.course.changed_by = self.user
self.course.save()
def test_update_all_revision_with_invalid_id(self):
"""Verify that api return 404 error if revision_id does not exists. """
response = self._update_all_by_revision_course(0000)
self.assertEqual(response.status_code, 404)
def test_update_all_course_revision_without_authentication(self):
"""Verify that api return authentication error if user is not logged in. """
self.client.logout()
revision = self.course.history.first()
response = self._update_all_by_revision_course(revision.history_id)
self.assertEqual(response.status_code, 403)
def test_update_all_course_revision(self):
"""Verify that api update the course with the according to the revision id. """
# most recent history revision made by user
revision = self.course.history.latest()
self.assertEqual(revision.changed_by, self.user)
self.client.logout()
# update the course through api and now change-by and history user will the 2nd user.
user_2 = UserFactory()
self.client.login(username=user_2.username, password=USER_PASSWORD)
response = self._update_all_by_revision_course(revision.history_id)
self.assertEqual(response.status_code, 201)
revision = self.course.history.latest()
self.assertEqual(revision.history_user, user_2)
course = Course.objects.get(id=self.course.id)
self.assertEqual(course.changed_by, user_2)
def _update_all_by_revision_course(self, revision_id):
"""Update the course objects by changing just changed-by attr."""
course_revision_path = reverse(
'publisher:api:accept_all_revision', kwargs={'history_id': revision_id}
)
return self.client.post(path=course_revision_path)
""" Publisher API URLs. """ """ Publisher API URLs. """
from django.conf.urls import url from django.conf.urls import url
from course_discovery.apps.publisher.api.views import (ChangeCourseRunStateView, ChangeCourseStateView, from course_discovery.apps.publisher.api.views import (AcceptAllRevisionView, ChangeCourseRunStateView,
CourseRevisionDetailView, CourseRoleAssignmentView, ChangeCourseStateView, CourseRevisionDetailView,
CoursesAutoComplete, OrganizationGroupUserView, CourseRoleAssignmentView, CoursesAutoComplete,
RevertCourseRevisionView, UpdateCourseRunView) OrganizationGroupUserView, RevertCourseRevisionView,
UpdateCourseRunView)
urlpatterns = [ urlpatterns = [
url(r'^course_role_assignments/(?P<pk>\d+)/$', CourseRoleAssignmentView.as_view(), name='course_role_assignments'), url(r'^course_role_assignments/(?P<pk>\d+)/$', CourseRoleAssignmentView.as_view(), name='course_role_assignments'),
...@@ -19,4 +20,8 @@ urlpatterns = [ ...@@ -19,4 +20,8 @@ urlpatterns = [
RevertCourseRevisionView.as_view(), name='course_revision_revert' RevertCourseRevisionView.as_view(), name='course_revision_revert'
), ),
url(r'^course-autocomplete/$', CoursesAutoComplete.as_view(), name='course-autocomplete'), url(r'^course-autocomplete/$', CoursesAutoComplete.as_view(), name='course-autocomplete'),
url(
r'^course/revision/(?P<history_id>\d+)/accept_revision/$',
AcceptAllRevisionView.as_view(), name='accept_all_revision'
),
] ]
...@@ -86,6 +86,7 @@ class RevertCourseRevisionView(APIView): ...@@ -86,6 +86,7 @@ class RevertCourseRevisionView(APIView):
if field not in ['team_admin', 'organization', 'add_new_run']: if field not in ['team_admin', 'organization', 'add_new_run']:
setattr(course, field, getattr(history_object, field)) setattr(course, field, getattr(history_object, field))
course.changed_by = self.request.user
course.save() course.save()
except: # pylint: disable=bare-except except: # pylint: disable=bare-except
logger.exception('Unable to revert the course [%s] for revision [%s].', course.id, history_id) logger.exception('Unable to revert the course [%s] for revision [%s].', course.id, history_id)
...@@ -117,3 +118,19 @@ class CoursesAutoComplete(LoginRequiredMixin, autocomplete.Select2QuerySetView): ...@@ -117,3 +118,19 @@ class CoursesAutoComplete(LoginRequiredMixin, autocomplete.Select2QuerySetView):
return qs return qs
return [] return []
class AcceptAllRevisionView(APIView):
""" Generate history version. """
permission_classes = (IsAuthenticated, )
def post(self, request, history_id): # pylint: disable=unused-argument
""" Update the course against the given revision id. """
history_object = get_object_or_404(historicalcourse, pk=history_id)
course = get_object_or_404(Course, id=history_object.id)
course.changed_by = self.request.user
course.save()
return Response(status=status.HTTP_201_CREATED)
...@@ -1905,6 +1905,58 @@ class CourseDetailViewTests(TestCase): ...@@ -1905,6 +1905,58 @@ class CourseDetailViewTests(TestCase):
self.assertContains(response, 'REVISION HISTORY') self.assertContains(response, 'REVISION HISTORY')
def test_detail_page_without_most_recent_revision(self):
"""
Test that user can see history widget on detail page if history exists.
"""
self._assign_user_permission()
response = self.client.get(self.detail_page_url)
self.assertNotIn('most_recent_revision_id', response.context)
self.assertNotIn('accept_all_button', response.context)
def test_detail_page_with_accept_button(self):
"""
Test that user can see accept button and context has most recent revision id
"""
self._assign_user_permission()
# update course object through page so that it will create history objects properly.
# otherwise history_user does not appear in table.
self._post_data(self.organization_extension)
self._post_data(self.organization_extension)
response = self.client.get(self.detail_page_url)
current_user_revision = self.course.history.latest().history_id
self.assertEqual(response.context['most_recent_revision_id'], current_user_revision)
self.assertNotIn('accept_all_button', response.context)
# it will make another history object without any history_user object.
self.course.save()
response = self.client.get(self.detail_page_url)
self.assertEqual(response.context['most_recent_revision_id'], current_user_revision)
self.assertTrue(response.context['accept_all_button'])
def _assign_user_permission(self):
""" Assign permissions."""
self.user.groups.add(self.organization_extension.group)
assign_perm(OrganizationExtension.VIEW_COURSE, self.organization_extension.group, self.organization_extension)
assign_perm(
OrganizationExtension.EDIT_COURSE, self.organization_extension.group, self.organization_extension
)
def _post_data(self, organization_extension):
"""
Generate post data and return.
"""
post_data = model_to_dict(self.course)
post_data.pop('image')
post_data['team_admin'] = self.user.id
post_data['organization'] = organization_extension.organization.id
post_data['title'] = 'updated title'
self.client.post(reverse('publisher:publisher_courses_edit', args=[self.course.id]), post_data)
@ddt.ddt @ddt.ddt
class CourseEditViewTests(TestCase): class CourseEditViewTests(TestCase):
......
...@@ -442,6 +442,22 @@ class CourseDetailView(mixins.LoginRequiredMixin, mixins.PublisherPermissionMixi ...@@ -442,6 +442,22 @@ class CourseDetailView(mixins.LoginRequiredMixin, mixins.PublisherPermissionMixi
if current_owner_role.role == PublisherUserRole.MarketingReviewer if current_owner_role.role == PublisherUserRole.MarketingReviewer
else _('marketing')) else _('marketing'))
history_list = self.object.history.all().order_by('history_id')
# Find out history of a logged-in user from the history list and if there is any other latest history
# from other users then show accept changes button.
if history_list and history_list.filter(history_user=self.request.user).exists():
logged_in_user_history = history_list.filter(history_user=self.request.user).latest()
context['most_recent_revision_id'] = (
logged_in_user_history.history_id if logged_in_user_history else None
)
if history_list.latest().history_id > logged_in_user_history.history_id:
context['accept_all_button'] = (
current_owner_role.role == PublisherUserRole.CourseTeam and
current_owner_role.user == self.request.user
)
return context return context
...@@ -761,6 +777,7 @@ class CourseRevisionView(mixins.LoginRequiredMixin, DetailView): ...@@ -761,6 +777,7 @@ class CourseRevisionView(mixins.LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CourseRevisionView, self).get_context_data(**kwargs) context = super(CourseRevisionView, self).get_context_data(**kwargs)
try: try:
context['history_object'] = self.object.history.get(history_id=self.kwargs.get('revision_id')) context['history_object'] = self.object.history.get(history_id=self.kwargs.get('revision_id'))
except ObjectDoesNotExist: except ObjectDoesNotExist:
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-19 19:30+0500\n" "POT-Creation-Date: 2017-05-22 17:08+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: apps/api/filters.py #: apps/api/filters.py
#, python-brace-format #, python-brace-format
...@@ -1272,6 +1272,10 @@ msgstr "" ...@@ -1272,6 +1272,10 @@ msgstr ""
msgid "Unable to revert the revision, Please try again later." msgid "Unable to revert the revision, Please try again later."
msgstr "" msgstr ""
#: templates/publisher/_history_widget.html
msgid "Accept All"
msgstr ""
#: templates/publisher/_render_optional_field.html #: templates/publisher/_render_optional_field.html
#: templates/publisher/course_detail.html #: templates/publisher/course_detail.html
#: templates/publisher/course_run_detail/_all.html #: templates/publisher/course_run_detail/_all.html
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-19 19:30+0500\n" "POT-Creation-Date: 2017-05-22 17:08+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: static/js/catalogs-change-form.js #: static/js/catalogs-change-form.js
msgid "Preview" msgid "Preview"
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-19 19:30+0500\n" "POT-Creation-Date: 2017-05-22 17:08+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/api/filters.py #: apps/api/filters.py
...@@ -1453,6 +1453,10 @@ msgstr "" ...@@ -1453,6 +1453,10 @@ msgstr ""
"Ûnäßlé tö révért thé révïsïön, Pléäsé trý ägäïn lätér. Ⱡ'σяєм ιρѕυм ∂σłσя " "Ûnäßlé tö révért thé révïsïön, Pléäsé trý ägäïn lätér. Ⱡ'σяєм ιρѕυм ∂σłσя "
"ѕιт αмєт, ¢σηѕє¢тєтυя α#" "ѕιт αмєт, ¢σηѕє¢тєтυя α#"
#: templates/publisher/_history_widget.html
msgid "Accept All"
msgstr "Àççépt Àll Ⱡ'σяєм ιρѕυм ∂σłσ#"
#: templates/publisher/_render_optional_field.html #: templates/publisher/_render_optional_field.html
#: templates/publisher/course_detail.html #: templates/publisher/course_detail.html
#: templates/publisher/course_run_detail/_all.html #: templates/publisher/course_run_detail/_all.html
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-19 19:30+0500\n" "POT-Creation-Date: 2017-05-22 17:08+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: static/js/catalogs-change-form.js #: static/js/catalogs-change-form.js
......
...@@ -19,14 +19,14 @@ function getComparableText(object) { ...@@ -19,14 +19,14 @@ function getComparableText(object) {
if ($(object).find('.dont-compare').length > 0) { if ($(object).find('.dont-compare').length > 0) {
return ""; return "";
} else { } else {
return object.text().trim() return object.html().trim()
} }
} }
var dmp = new diff_match_patch(); var dmp = new diff_match_patch();
dmp.Diff_EditCost = 8; dmp.Diff_EditCost = 8;
function showDiff($object, $historyObject, $outputDiv) { function showDiff($object, $historyObject, $outputDiv) {
var currentText = $($.parseHTML($object.text())).text().trim(), var currentText = $object.html().trim(),
historyText = getComparableText($historyObject), historyText = getComparableText($historyObject),
diff; diff;
...@@ -40,7 +40,12 @@ function showDiff($object, $historyObject, $outputDiv) { ...@@ -40,7 +40,12 @@ function showDiff($object, $historyObject, $outputDiv) {
function showDiffCourseDetails(currentObject, historyObject, $outputDiv) { function showDiffCourseDetails(currentObject, historyObject, $outputDiv) {
var d = dmp.diff_main(currentObject, historyObject); var d = dmp.diff_main(currentObject, historyObject);
dmp.diff_cleanupEfficiency(d); dmp.diff_cleanupEfficiency(d);
$outputDiv.html(dmp.diff_prettyHtml(d)); $outputDiv.append(decodeEntities(dmp.diff_prettyHtml(d)));
$outputDiv.show(); $outputDiv.show();
} }
function decodeEntities(encodedString) {
var textArea = document.createElement('textarea');
textArea.innerHTML = encodedString;
return textArea.value;
}
...@@ -5,6 +5,7 @@ $(document).on('change', '#id_select_revisions', function (e) { ...@@ -5,6 +5,7 @@ $(document).on('change', '#id_select_revisions', function (e) {
var btn_edit = $('#btn_course_edit'); var btn_edit = $('#btn_course_edit');
var current_btn_edit_url = btn_edit.attr('href'); var current_btn_edit_url = btn_edit.attr('href');
var btn_accept_revision = $('#btn_accept_revision');
if (revisionUrl) { if (revisionUrl) {
loadRevisionHistory(revisionUrl); loadRevisionHistory(revisionUrl);
...@@ -17,10 +18,19 @@ $(document).on('change', '#id_select_revisions', function (e) { ...@@ -17,10 +18,19 @@ $(document).on('change', '#id_select_revisions', function (e) {
if (this.selectedIndex > 0) { if (this.selectedIndex > 0) {
$('#span_revert_revision').show(); $('#span_revert_revision').show();
btn_edit.prop("href", $(this.selectedOptions).data('revisionId')); btn_edit.prop("href", $(this.selectedOptions).data('revisionId'));
var reversionValue = $('select#id_select_revisions option:selected').data('reversionValue');
//show accept-all button.
if (reversionValue === parseInt(btn_accept_revision.data('most-recent-revision-id'))){
btn_accept_revision.show();
}
else
btn_accept_revision.hide();
} }
else { else {
$('#span_revert_revision').hide(); $('#span_revert_revision').hide();
btn_edit.prop("href", current_btn_edit_url.split('?history')[0]); btn_edit.prop("href", current_btn_edit_url.split('?history')[0]);
btn_accept_revision.hide();
} }
}); });
...@@ -66,3 +76,21 @@ $(document).on('click', '#id_confirm_revert_revision', function (e) { ...@@ -66,3 +76,21 @@ $(document).on('click', '#id_confirm_revert_revision', function (e) {
} }
}); });
}); });
$(document).on('click', '#btn_accept_revision', function (e) {
// after the confirmation update the course according to the history id
e.preventDefault();
var acceptRevisionUrl = $('select#id_select_revisions option:selected').data('acceptRevision');
// $('#confirmationModal').show();
$.ajax({
type: "post",
url: acceptRevisionUrl,
success: function (response) {
location.reload();
},
error: function () {
$('#AcceptAllAlert').show();
}
});
});
...@@ -26,4 +26,8 @@ $(document).ready(function() { ...@@ -26,4 +26,8 @@ $(document).ready(function() {
$('#userRoleContainer-' + roleName).hide(); $('#userRoleContainer-' + roleName).hide();
}); });
$('#btn_accept_revision').hide();
$("#id_select_revisions ").find(
'[data-reversion-value="' + $('#btn_accept_revision').data('most-recent-revision-id') + '"]'
).attr("selected", "selected").change();
}); });
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
.margin-top20 { .margin-top20 {
margin-top: 20px; margin-top: 20px;
margin-bottom: 10px;
} }
.hidden { .hidden {
......
...@@ -14,10 +14,13 @@ ...@@ -14,10 +14,13 @@
{% url 'publisher:publisher_course_revision' course.id history.history_id as course_revision_url %} {% url 'publisher:publisher_course_revision' course.id history.history_id as course_revision_url %}
{% url 'publisher:api:course_revision_revert' history.history_id as revision_revert_url %} {% url 'publisher:api:course_revision_revert' history.history_id as revision_revert_url %}
{% url 'publisher:publisher_courses_edit' pk=object.id as revision_id_url %} {% url 'publisher:publisher_courses_edit' pk=object.id as revision_id_url %}
{% url 'publisher:api:accept_all_revision' history.history_id as accept_revision_url %}
<option data-revision-url="{{ revision_url }}" <option data-revision-url="{{ revision_url }}"
value="{{ course_revision_url }}" value="{{ course_revision_url }}"
data-revert-url="{{ revision_revert_url }}" data-revert-url="{{ revision_revert_url }}"
data-revision-id="{{ revision_id_url }}?history_id={{ history.history_id }}" > data-revision-id="{{ revision_id_url }}?history_id={{ history.history_id }}"
data-reversion-value="{{ history.history_id }}"
data-accept-revision="{{ accept_revision_url }}" >
{% blocktrans with history.history_date|date:'d-m-Y' as history_date and history.changed_by as changed_by trimmed %} {% blocktrans with history.history_date|date:'d-m-Y' as history_date and history.changed_by as changed_by trimmed %}
{{ history_date }}&nbsp;by&nbsp;{{ changed_by}} {{ history_date }}&nbsp;by&nbsp;{{ changed_by}}
{% endblocktrans %} {% endblocktrans %}
...@@ -25,9 +28,19 @@ ...@@ -25,9 +28,19 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
<br>
{% if can_edit %}
<span id="span_revert_revision" class="hidden"> <span id="span_revert_revision" class="hidden">
<a id="id_revert_revision" class="btn btn-brand btn-small btn-courserun-add btn-revision">{% trans "Restore to this version" %}</a> <a id="id_revert_revision" class="btn btn-brand btn-small btn-courserun-add btn-revision">{% trans "Restore to this version" %}</a>
</span> </span>
<div id="RevertRevisionAlert" class="alert alert-error hidden" role="alert">{% trans "Unable to revert the revision, Please try again later." %}</div> <div id="RevertRevisionAlert" class="alert alert-error hidden" role="alert">{% trans "Unable to revert the revision, Please try again later." %}</div>
{% if accept_all_button %}
<a id="btn_accept_revision" type="button" data-most-recent-revision-id="{{ most_recent_revision_id }}" class="hidden btn btn-brand btn-small btn-courserun-add btn-revision">
{% trans "Accept All" %}
</a>
{% endif %}
{% endif %}
</div> </div>
{% include 'publisher/_revert_confirmation.html' %} {% include 'publisher/_revert_confirmation.html' %}
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