Commit e11c1d12 by Zia Fazal

certificate edit/delete is only allowed if user is edX PM or certificate is not active

removed org helper
parent 3a6b2c30
...@@ -178,6 +178,7 @@ class CertificateManager(object): ...@@ -178,6 +178,7 @@ class CertificateManager(object):
"description": certificate_data['description'], "description": certificate_data['description'],
"version": CERTIFICATE_SCHEMA_VERSION, "version": CERTIFICATE_SCHEMA_VERSION,
"org_logo_path": certificate_data.get('org_logo_path', ''), "org_logo_path": certificate_data.get('org_logo_path', ''),
"is_active": certificate_data.get('is_active', False),
"signatories": certificate_data['signatories'] "signatories": certificate_data['signatories']
} }
...@@ -209,13 +210,17 @@ class CertificateManager(object): ...@@ -209,13 +210,17 @@ class CertificateManager(object):
return certificate return certificate
@staticmethod @staticmethod
def get_certificates(course): def get_certificates(course, only_active=False):
""" """
Retrieve the certificates list from the provided course Retrieve the certificates list from the provided course,
if `only_active` is True it would skip inactive certificates.
""" """
# The top-level course field is 'certificates', which contains various properties, # The top-level course field is 'certificates', which contains various properties,
# including the actual 'certificates' list that we're working with in this context # including the actual 'certificates' list that we're working with in this context
return course.certificates.get('certificates', []) certificates = course.certificates.get('certificates', [])
if only_active:
certificates = [certificate for certificate in certificates if certificate['is_active']]
return certificates
@staticmethod @staticmethod
def remove_certificate(request, store, course, certificate_id): def remove_certificate(request, store, course, certificate_id):
...@@ -436,6 +441,12 @@ def certificates_detail_handler(request, course_key_string, certificate_id): ...@@ -436,6 +441,12 @@ def certificates_detail_handler(request, course_key_string, certificate_id):
store = modulestore() store = modulestore()
if request.method in ('POST', 'PUT'): if request.method in ('POST', 'PUT'):
if certificate_id:
active_certificates = CertificateManager.get_certificates(course, only_active=True)
if int(certificate_id) in [int(certificate["id"]) for certificate in active_certificates]:
# Only global staff (PMs) are able to edit active certificate configuration
if not GlobalStaff().has_user(request.user):
raise PermissionDenied()
try: try:
new_certificate = CertificateManager.deserialize_certificate(course, request.body) new_certificate = CertificateManager.deserialize_certificate(course, request.body)
except CertificateValidationError as err: except CertificateValidationError as err:
...@@ -457,12 +468,15 @@ def certificates_detail_handler(request, course_key_string, certificate_id): ...@@ -457,12 +468,15 @@ def certificates_detail_handler(request, course_key_string, certificate_id):
return JsonResponse(serialized_certificate, status=201) return JsonResponse(serialized_certificate, status=201)
elif request.method == "DELETE": elif request.method == "DELETE":
# Only global staff (PMs) are able to activate/deactivate certificate configuration
if not GlobalStaff().has_user(request.user):
raise PermissionDenied()
if not match_cert: if not match_cert:
return JsonResponse(status=404) return JsonResponse(status=404)
active_certificates = CertificateManager.get_certificates(course, only_active=True)
if int(certificate_id) in [int(certificate["id"]) for certificate in active_certificates]:
# Only global staff (PMs) are able to delete active certificate configuration
if not GlobalStaff().has_user(request.user):
raise PermissionDenied()
CertificateManager.remove_certificate( CertificateManager.remove_certificate(
request=request, request=request,
store=store, store=store,
......
...@@ -67,7 +67,7 @@ class HelperMethods(object): ...@@ -67,7 +67,7 @@ class HelperMethods(object):
) )
contentstore().save(content) contentstore().save(content)
def _add_course_certificates(self, count=1, signatory_count=0): def _add_course_certificates(self, count=1, signatory_count=0, is_active=False):
""" """
Create certificate for the course. Create certificate for the course.
""" """
...@@ -96,7 +96,7 @@ class HelperMethods(object): ...@@ -96,7 +96,7 @@ class HelperMethods(object):
'org_logo_path': '/c4x/test/CSS101/asset/org_logo{}.png'.format(i), 'org_logo_path': '/c4x/test/CSS101/asset/org_logo{}.png'.format(i),
'signatories': signatories, 'signatories': signatories,
'version': CERTIFICATE_SCHEMA_VERSION, 'version': CERTIFICATE_SCHEMA_VERSION,
'is_active': False 'is_active': is_active
} for i in xrange(0, count) } for i in xrange(0, count)
] ]
self._create_fake_images([certificate['org_logo_path'] for certificate in certificates]) self._create_fake_images([certificate['org_logo_path'] for certificate in certificates])
...@@ -221,6 +221,7 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat ...@@ -221,6 +221,7 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat
u'name': u'Test certificate', u'name': u'Test certificate',
u'description': u'Test description', u'description': u'Test description',
u'org_logo_path': '', u'org_logo_path': '',
u'is_active': False,
u'signatories': [] u'signatories': []
} }
response = self.client.ajax_post( response = self.client.ajax_post(
...@@ -389,6 +390,7 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific ...@@ -389,6 +390,7 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific
u'description': u'Test description', u'description': u'Test description',
u'course_title': u'Course Title Override', u'course_title': u'Course Title Override',
u'org_logo_path': '', u'org_logo_path': '',
u'is_active': False,
u'signatories': [] u'signatories': []
} }
...@@ -420,6 +422,7 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific ...@@ -420,6 +422,7 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific
u'description': u'New test description', u'description': u'New test description',
u'course_title': u'Course Title Override', u'course_title': u'Course Title Override',
u'org_logo_path': '', u'org_logo_path': '',
u'is_active': False,
u'signatories': [] u'signatories': []
} }
...@@ -494,9 +497,9 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific ...@@ -494,9 +497,9 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific
def test_delete_certificate_without_global_staff_permissions(self): def test_delete_certificate_without_global_staff_permissions(self):
""" """
Tests certificate deletion without global staff permission on course. Tests deletion of an active certificate without global staff permission on course.
""" """
self._add_course_certificates(count=2, signatory_count=1) self._add_course_certificates(count=2, signatory_count=1, is_active=True)
user = UserFactory() user = UserFactory()
for role in [CourseInstructorRole, CourseStaffRole]: for role in [CourseInstructorRole, CourseStaffRole]:
role(self.course.id).add_users(user) role(self.course.id).add_users(user)
...@@ -509,6 +512,34 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific ...@@ -509,6 +512,34 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific
) )
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_update_active_certificate_without_global_staff_permissions(self):
"""
Tests update of an active certificate without global staff permission on course.
"""
self._add_course_certificates(count=2, signatory_count=1, is_active=True)
cert_data = {
u'id': 1,
u'version': CERTIFICATE_SCHEMA_VERSION,
u'name': u'New test certificate',
u'description': u'New test description',
u'course_title': u'Course Title Override',
u'org_logo_path': '',
u'is_active': False,
u'signatories': []
}
user = UserFactory()
for role in [CourseInstructorRole, CourseStaffRole]:
role(self.course.id).add_users(user)
self.client.login(username=user.username, password='test')
response = self.client.put(
self._url(cid=1),
data=json.dumps(cert_data),
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(response.status_code, 403)
def test_delete_non_existing_certificate(self): def test_delete_non_existing_certificate(self):
""" """
Try to delete a non existing certificate. It should return status code 404 Not found. Try to delete a non existing certificate. It should return status code 404 Not found.
......
...@@ -80,8 +80,8 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails ...@@ -80,8 +80,8 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails
this.model = new CertificateModel({ this.model = new CertificateModel({
name: 'Test Name', name: 'Test Name',
description: 'Test Description', description: 'Test Description',
course_title: 'Test Course Title Override' course_title: 'Test Course Title Override',
is_active: true
}, this.newModelOptions); }, this.newModelOptions);
this.collection = new CertificatesCollection([ this.model ], { this.collection = new CertificatesCollection([ this.model ], {
...@@ -139,11 +139,17 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails ...@@ -139,11 +139,17 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails
expect(this.model.get('editing')).toBe(true); expect(this.model.get('editing')).toBe(true);
}); });
it('should not present a Edit action if user is not global staff and certificate is active', function () {
window.CMS.User = {isGlobalStaff: false};
appendSetFixtures(this.view.render().el);
expect(this.view.$('.action-edit .edit')).not.toExist();
});
it('should present a Delete action', function () { it('should present a Delete action', function () {
expect(this.view.$('.action-delete .delete')).toExist(); expect(this.view.$('.action-delete .delete')).toExist();
}); });
it('should not present a Delete action if user is not global staff', function () { it('should not present a Delete action if user is not global staff and certificate is active', function () {
window.CMS.User = {isGlobalStaff: false}; window.CMS.User = {isGlobalStaff: false};
appendSetFixtures(this.view.render().el); appendSetFixtures(this.view.render().el);
expect(this.view.$('.action-delete .delete')).not.toExist(); expect(this.view.$('.action-delete .delete')).not.toExist();
...@@ -159,7 +165,7 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails ...@@ -159,7 +165,7 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails
describe('Signatory details', function(){ describe('Signatory details', function(){
beforeEach(function() { beforeEach(function() {
this.view.render(true); this.view.render();
}); });
it('displays certificate signatories details', function(){ it('displays certificate signatories details', function(){
...@@ -171,6 +177,16 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails ...@@ -171,6 +177,16 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails
).toContainText('Organization of the signatory'); ).toContainText('Organization of the signatory');
}); });
it('should present Edit action on signaotry', function () {
expect(this.view.$(SELECTORS.edit_signatory)).toExist();
});
it('should not present Edit action on signaotry if user is not global staff and certificate is active', function () {
window.CMS.User = {isGlobalStaff: false};
this.view.render();
expect(this.view.$(SELECTORS.edit_signatory)).not.toExist();
});
it('supports in-line editing of signatory information', function() { it('supports in-line editing of signatory information', function() {
this.view.$(SELECTORS.edit_signatory).click(); this.view.$(SELECTORS.edit_signatory).click();
......
...@@ -113,7 +113,8 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce ...@@ -113,7 +113,8 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce
this.newModelOptions = {add: true}; this.newModelOptions = {add: true};
this.model = new CertificateModel({ this.model = new CertificateModel({
name: 'Test Name', name: 'Test Name',
description: 'Test Description' description: 'Test Description',
is_active: true
}, this.newModelOptions); }, this.newModelOptions);
...@@ -151,7 +152,7 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce ...@@ -151,7 +152,7 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce
expect(this.view.$('.action-delete')).toExist(); expect(this.view.$('.action-delete')).toExist();
}); });
it('should not have delete button is user is not global staff', function() { it('should not have delete button if user is not global staff and certificate is active', function() {
window.CMS.User = {isGlobalStaff: false}; window.CMS.User = {isGlobalStaff: false};
appendSetFixtures(this.view.render().el); appendSetFixtures(this.view.render().el);
expect(this.view.$('.action-delete')).not.toExist(); expect(this.view.$('.action-delete')).not.toExist();
......
...@@ -102,6 +102,7 @@ function($, _, Backbone, gettext, ...@@ -102,6 +102,7 @@ function($, _, Backbone, gettext,
description: this.model.escape('description'), description: this.model.escape('description'),
course_title: this.model.escape('course_title'), course_title: this.model.escape('course_title'),
org_logo_path: this.model.escape('org_logo_path'), org_logo_path: this.model.escape('org_logo_path'),
is_active: this.model.escape('is_active'),
isNew: this.model.isNew() isNew: this.model.isNew()
}; };
}, },
......
...@@ -30,10 +30,10 @@ ...@@ -30,10 +30,10 @@
</ol> </ol>
<ul class="actions certificate-actions"> <ul class="actions certificate-actions">
<% if (CMS.User.isGlobalStaff || !is_active) { %>
<li class="action action-edit"> <li class="action action-edit">
<button class="edit"><i class="icon fa fa-pencil" aria-hidden="true"></i> <%= gettext("Edit") %></button> <button class="edit"><i class="icon fa fa-pencil" aria-hidden="true"></i> <%= gettext("Edit") %></button>
</li> </li>
<% if (CMS.User.isGlobalStaff) { %>
<li class="action action-delete wrapper-delete-button" data-tooltip="<%= gettext('Delete') %>"> <li class="action action-delete wrapper-delete-button" data-tooltip="<%= gettext('Delete') %>">
<button class="delete action-icon"><i class="icon fa fa-trash-o" aria-hidden="true"></i><span><%= gettext("Delete") %></span></button> <button class="delete action-icon"><i class="icon fa fa-trash-o" aria-hidden="true"></i><span><%= gettext("Delete") %></span></button>
</li> </li>
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
<div class="actions"> <div class="actions">
<button class="action action-primary" type="submit"><% if (isNew) { print(gettext("Create")) } else { print(gettext("Save")) } %></button> <button class="action action-primary" type="submit"><% if (isNew) { print(gettext("Create")) } else { print(gettext("Save")) } %></button>
<button class="action action-secondary action-cancel"><%= gettext("Cancel") %></button> <button class="action action-secondary action-cancel"><%= gettext("Cancel") %></button>
<% if (!isNew && CMS.User.isGlobalStaff) { %> <% if (!isNew && (CMS.User.isGlobalStaff || !is_active)) { %>
<span class="wrapper-delete-button"> <span class="wrapper-delete-button">
<a class="button action-delete delete" href="#"><%= gettext("Delete") %></a> <a class="button action-delete delete" href="#"><%= gettext("Delete") %></a>
</span> </span>
......
<div class="signatory-panel-default"> <div class="signatory-panel-default">
<% if (CMS.User.isGlobalStaff || !certificate.get('is_active')) { %>
<div class="actions certificate-actions signatory-panel-edit"> <div class="actions certificate-actions signatory-panel-edit">
<span class="action action-edit-signatory"> <span class="action action-edit-signatory">
<a href="javascript:void(0);" class="edit-signatory"><i class="icon fa fa-pencil" aria-hidden="true"></i> <%= gettext("Edit") %></a> <a href="javascript:void(0);" class="edit-signatory"><i class="icon fa fa-pencil" aria-hidden="true"></i> <%= gettext("Edit") %></a>
</span> </span>
</div> </div>
<% } %>
<div class="signatory-panel-header">Signatory <%= signatory_number %>&nbsp;</div> <div class="signatory-panel-header">Signatory <%= signatory_number %>&nbsp;</div>
<div class="signatory-panel-body"> <div class="signatory-panel-body">
<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