Commit 8e08e85c by Awais Committed by Awais Qureshi

Adding revisions history revert button.

ECOM-7776
parent ec2af959
......@@ -6,18 +6,23 @@ from django.contrib.auth.models import Group
from django.contrib.sites.models import Site
from django.core import mail
from django.core.urlresolvers import reverse
from django.db import IntegrityError
from django.test import TestCase
from guardian.shortcuts import assign_perm
from mock import patch
from opaque_keys.edx.keys import CourseKey
from testfixtures import LogCapture
from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory
from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.tests import toggle_switch
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory, PersonFactory
from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api import views
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.constants import INTERNAL_USER_GROUP_NAME
from course_discovery.apps.publisher.models import CourseRun, CourseRunState, CourseState, OrganizationExtension, Seat
from course_discovery.apps.publisher.models import (Course, CourseRun, CourseRunState, CourseState,
OrganizationExtension, Seat)
from course_discovery.apps.publisher.tests import JSON_CONTENT_TYPE, factories
......@@ -756,3 +761,67 @@ class ChangeCourseRunStateViewTests(TestCase):
)
self.assertEqual(str(mail.outbox[0].subject), expected_subject)
self.assertIn('has been published', mail.outbox[0].body.strip())
class RevertCourseByRevisionTests(TestCase):
def setUp(self):
super(RevertCourseByRevisionTests, self).setUp()
self.course = factories.CourseFactory(title='first title')
# update title so that another revision created
self.course.title = "updated title"
self.course.save()
self.user = UserFactory()
self.client.login(username=self.user.username, password=USER_PASSWORD)
def test_revert_course_revision_with_invalid_id(self):
"""Verify that api return 404 error if revision_id does not exists. """
response = self._revert_course(0000)
self.assertEqual(response.status_code, 404)
def test_revert_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._revert_course(revision.history_id)
self.assertEqual(response.status_code, 403)
def test_revert_course_revision(self):
"""Verify that api update the course with the according to the revision id. """
revision = self.course.history.last()
self.assertNotEqual(revision.title, self.course.title)
response = self._revert_course(revision.history_id)
self.assertEqual(response.status_code, 204)
course = Course.objects.get(id=self.course.id)
self.assertEqual(revision.title, course.title)
def test_update_with_error(self):
""" Verify that in case of any error api returns proper error message and code."""
with LogCapture(views.logger.name) as l:
with patch.object(Course, "save") as mock_method:
mock_method.side_effect = IntegrityError
revision = self.course.history.last()
response = self._revert_course(revision.history_id)
l.check(
(
views.logger.name,
'ERROR',
'Unable to revert the course [{}] for revision [{}].'.format(
self.course.id,
revision.history_id
)
)
)
self.assertEqual(response.status_code, 400)
def _revert_course(self, revision_id):
"""Returns response of api against given revision_id."""
course_revision_path = reverse(
'publisher:api:course_revision_revert', kwargs={'history_id': revision_id}
)
return self.client.put(path=course_revision_path)
""" Publisher API URLs. """
from django.conf.urls import url
from course_discovery.apps.publisher.api.views import (
ChangeCourseRunStateView, ChangeCourseStateView, CourseRevisionDetailView, CourseRoleAssignmentView,
OrganizationGroupUserView, UpdateCourseRunView
)
from course_discovery.apps.publisher.api.views import (ChangeCourseRunStateView, ChangeCourseStateView,
CourseRevisionDetailView, CourseRoleAssignmentView,
OrganizationGroupUserView, RevertCourseRevisionView,
UpdateCourseRunView)
urlpatterns = [
url(r'^course_role_assignments/(?P<pk>\d+)/$', CourseRoleAssignmentView.as_view(), name='course_role_assignments'),
......@@ -14,4 +14,8 @@ urlpatterns = [
url(r'^course_runs/(?P<pk>\d+)/$', UpdateCourseRunView.as_view(), name='update_course_run'),
url(r'^course_revisions/(?P<history_id>\d+)/$', CourseRevisionDetailView.as_view(), name='course_revisions'),
url(r'^course_run_state/(?P<pk>\d+)/$', ChangeCourseRunStateView.as_view(), name='change_course_run_state'),
url(
r'^course/revision/(?P<history_id>\d+)/revert/$',
RevertCourseRevisionView.as_view(), name='course_revision_revert'
),
]
import logging
from django.apps import apps
from rest_framework import status
from rest_framework.generics import ListAPIView, RetrieveAPIView, UpdateAPIView, get_object_or_404
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.api.permissions import (CanViewAssociatedCourse, InternalUserPermission,
......@@ -7,9 +13,14 @@ from course_discovery.apps.publisher.api.permissions import (CanViewAssociatedCo
from course_discovery.apps.publisher.api.serializers import (CourseRevisionSerializer, CourseRunSerializer,
CourseRunStateSerializer, CourseStateSerializer,
CourseUserRoleSerializer, GroupUserSerializer)
from course_discovery.apps.publisher.forms import CustomCourseForm
from course_discovery.apps.publisher.models import (Course, CourseRun, CourseRunState, CourseState, CourseUserRole,
OrganizationExtension)
logger = logging.getLogger(__name__)
historicalcourse = apps.get_model('publisher', 'historicalcourse')
class CourseRoleAssignmentView(UpdateAPIView):
""" Update view for CourseUserRole """
......@@ -56,3 +67,24 @@ class ChangeCourseRunStateView(UpdateAPIView):
permission_classes = (IsAuthenticated, PublisherUserPermission,)
queryset = CourseRunState.objects.all()
serializer_class = CourseRunStateSerializer
class RevertCourseRevisionView(APIView):
""" Revert view for Course against a history version """
permission_classes = (IsAuthenticated, )
def put(self, request, history_id): # pylint: disable=unused-argument
""" Update the course version 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)
try:
for field in CustomCourseForm().fields:
if field not in ['team_admin', 'organization', 'add_new_run']:
setattr(course, field, getattr(history_object, field))
course.save()
except: # pylint: disable=bare-except
logger.exception('Unable to revert the course [%s] for revision [%s].', course.id, history_id)
return Response(status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
......@@ -7,14 +7,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-05 13:13+0500\n"
"POT-Creation-Date: 2017-05-08 16:26+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: apps/api/filters.py
#, python-brace-format
......@@ -991,6 +991,7 @@ msgstr ""
#: templates/metadata/admin/course_run.html
#: templates/publisher/_add_instructor_popup.html
#: templates/publisher/_revert_confirmation.html
#: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
#: templates/publisher/course_detail/_edit_warning_popup.html
......@@ -1235,9 +1236,18 @@ msgid "%(history_date)s&nbsp;by&nbsp;%(changed_by)s"
msgstr ""
#: templates/publisher/_history_widget.html
#: templates/publisher/_revert_confirmation.html
msgid "Restore to this version"
msgstr ""
#: templates/publisher/_history_widget.html
msgid "Open Selected Version"
msgstr ""
#: templates/publisher/_history_widget.html
msgid "Unable to revert the revision, Please try again later."
msgstr ""
#: templates/publisher/_render_optional_field.html
#: templates/publisher/course_detail.html
#: templates/publisher/course_run_detail/_all.html
......@@ -1256,6 +1266,16 @@ msgstr ""
msgid "(Required) Not yet added"
msgstr ""
#: templates/publisher/_revert_confirmation.html
#: templates/publisher/course_detail/_edit_warning_popup.html
#: templates/publisher/course_run_detail/_edit_warning.html
msgid "CAUTION"
msgstr ""
#: templates/publisher/_revert_confirmation.html
msgid "Are you sure you want to revert all changes to this version ?"
msgstr ""
#: templates/publisher/add_course_form.html templates/publisher/courses.html
msgid "Create New Course"
msgstr ""
......@@ -1519,11 +1539,6 @@ msgid "About Video Link"
msgstr ""
#: templates/publisher/course_detail/_edit_warning_popup.html
#: templates/publisher/course_run_detail/_edit_warning.html
msgid "CAUTION"
msgstr ""
#: templates/publisher/course_detail/_edit_warning_popup.html
#, python-format
msgid ""
"\n"
......
......@@ -7,14 +7,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-05 13:13+0500\n"
"POT-Creation-Date: 2017-05-08 16:26+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: static/js/catalogs-change-form.js
msgid "Preview"
......
......@@ -7,14 +7,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-05 13:13+0500\n"
"POT-Creation-Date: 2017-05-08 16:26+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/api/filters.py
......@@ -1151,6 +1151,7 @@ msgstr "Sävé Ⱡ'σяєм ι#"
#: templates/metadata/admin/course_run.html
#: templates/publisher/_add_instructor_popup.html
#: templates/publisher/_revert_confirmation.html
#: templates/publisher/add_course_form.html
#: templates/publisher/add_courserun_form.html
#: templates/publisher/course_detail/_edit_warning_popup.html
......@@ -1410,9 +1411,20 @@ msgid "%(history_date)s&nbsp;by&nbsp;%(changed_by)s"
msgstr "%(history_date)s&nbsp;ßý&nbsp;%(changed_by)s Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
#: templates/publisher/_history_widget.html
#: templates/publisher/_revert_confirmation.html
msgid "Restore to this version"
msgstr "Réstöré tö thïs vérsïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#"
#: templates/publisher/_history_widget.html
msgid "Open Selected Version"
msgstr "Öpén Séléçtéd Vérsïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: templates/publisher/_history_widget.html
msgid "Unable to revert the revision, Please try again later."
msgstr ""
"Ûnäßlé tö révért thé révïsïön, Pléäsé trý ägäïn lätér. Ⱡ'σяєм ιρѕυм ∂σłσя "
"ѕιт αмєт, ¢σηѕє¢тєтυя α#"
#: templates/publisher/_render_optional_field.html
#: templates/publisher/course_detail.html
#: templates/publisher/course_run_detail/_all.html
......@@ -1431,6 +1443,18 @@ msgstr "(Öptïönäl) Nöt ýét äddéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕ
msgid "(Required) Not yet added"
msgstr "(Réqüïréd) Nöt ýét äddéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#"
#: templates/publisher/_revert_confirmation.html
#: templates/publisher/course_detail/_edit_warning_popup.html
#: templates/publisher/course_run_detail/_edit_warning.html
msgid "CAUTION"
msgstr "ÇÀÛTÌÖN Ⱡ'σяєм ιρѕυм #"
#: templates/publisher/_revert_confirmation.html
msgid "Are you sure you want to revert all changes to this version ?"
msgstr ""
"Àré ýöü süré ýöü wänt tö révért äll çhängés tö thïs vérsïön ? Ⱡ'σяєм ιρѕυм "
"∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
#: templates/publisher/add_course_form.html templates/publisher/courses.html
msgid "Create New Course"
msgstr "Çréäté Néw Çöürsé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#"
......@@ -1769,11 +1793,6 @@ msgid "About Video Link"
msgstr "Àßöüt Vïdéö Lïnk Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#"
#: templates/publisher/course_detail/_edit_warning_popup.html
#: templates/publisher/course_run_detail/_edit_warning.html
msgid "CAUTION"
msgstr "ÇÀÛTÌÖN Ⱡ'σяєм ιρѕυм #"
#: templates/publisher/course_detail/_edit_warning_popup.html
#, python-format
msgid ""
"\n"
......
......@@ -7,14 +7,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-05 13:13+0500\n"
"POT-Creation-Date: 2017-05-08 16:26+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: static/js/catalogs-change-form.js
......
......@@ -9,6 +9,13 @@ $(document).on('change', '#id_select_revisions', function (e) {
$('.show-diff').hide();
$('.current').show();
}
//show revert button for any revision except current version.
if (this.selectedIndex > 0)
$('#span_revert_revision').show();
else
$('#span_revert_revision').hide();
});
function loadRevisionHistory(revisionUrl) {
......@@ -30,3 +37,25 @@ function loadRevisionHistory(revisionUrl) {
}
});
}
$(document).on('click', '#id_revert_revision', function (e) {
e.preventDefault();
$('#confirmationModal').show();
});
$(document).on('click', '#id_confirm_revert_revision', function (e) {
// after the confirmation update the course according to the history id
e.preventDefault();
var revertUrl = $('select#id_select_revisions option:selected').data('revertUrl');
$('#confirmationModal').show();
$.ajax({
type: "PUT",
url: revertUrl,
success: function (response) {
location.reload();
},
error: function () {
$('#RevertRevisionAlert').show();
}
});
});
......@@ -39,11 +39,21 @@
.btn-courserun-add {
@include padding(3px, 16px, 3px, 16px);
@include margin-left(10px);
@include margin-right(10px);
background-color: #169bd5;
border-color: #169bd5;
border-radius: 5px;
}
.select-revision {
margin-right: 10px;
}
.btn-revision {
@include margin-left(0);
margin-top: 8px;
}
.btn-accept {
@include padding-right(30px);
@include padding-left(30px);
......
......@@ -2,7 +2,7 @@
<div class="margin-top20">
<h5 class="hd-5 emphasized course-runs-heading">{% trans "REVISION HISTORY" %}</h5>
<br>
<select id="id_select_revisions">
<select id="id_select_revisions" class="select-revision">
{% for history in history_list %}
{% if forloop.first %}
<option value="{% url 'publisher:publisher_course_revision' course.id history.history_id %}">
......@@ -11,7 +11,7 @@
{% endblocktrans %}
</option>
{% else %}
<option data-revision-url="{% url 'publisher:api:course_revisions' history.history_id %}" value="{% url 'publisher:publisher_course_revision' course.id history.history_id %}">
<option data-revision-url="{% url 'publisher:api:course_revisions' history.history_id %}" value="{% url 'publisher:publisher_course_revision' course.id history.history_id %}" data-revert-url="{% url 'publisher:api:course_revision_revert' history.history_id %}">
{% 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}}
{% endblocktrans %}
......@@ -19,5 +19,10 @@
{% endif %}
{% endfor %}
</select>
<a id="id_open_revision" class="btn btn-brand btn-small btn-courserun-add" href="{% url 'publisher:publisher_course_revision' course.id history_list.first.history_id %}" target="_blank">{% trans "Open Selected Version" %}</a>
<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>
</span>
<a id="id_open_revision" class="btn btn-brand btn-small btn-courserun-add btn-revision" href="{% url 'publisher:publisher_course_revision' course.id history_list.first.history_id %}" target="_blank">{% trans "Open Selected Version" %}</a>
<div id="RevertRevisionAlert" class="alert alert-error hidden" role="alert">{% trans "Unable to revert the revision, Please try again later." %}</div>
</div>
{% include 'publisher/_revert_confirmation.html' %}
{% load i18n %}
<div id="confirmationModal" class="modal">
<div class="modal-content">
<h2 class="hd-2 emphasized">
<span class="icon fa fa-exclamation-triangle" aria-hidden="true"></span> {% trans "CAUTION" %}
</h2>
<span class="sr-only">CAUTION</span>
<p class="margin-top20">
{% trans "Are you sure you want to revert all changes to this version ?" %}
</p>
<div class="actions">
<a class="btn-cancel closeModal" href="#">{% trans "Cancel" %}</a>
<a id="id_confirm_revert_revision" class="btn-brand btn-base btn-accept" href="#">{% trans "Restore to this version" %}</a>
</div>
</div>
</div>
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