Commit e1ee5ac6 by Matt Drayer

mattdrayer/SOL-981: Integrate edx-organizations application

* asadiqbal08/SOL-1058: Add edx-organizations to certificate web view
  * Support organization logo asset management
  * Remove organization fields from Studio certificate configuration model

* SOL-981 pull request feedback fixes
parent 4e9b7ea9
......@@ -1033,7 +1033,6 @@ class CourseMetadataEditingTest(CourseTestCase):
'id': 1,
'name': 'Certificate Config Name',
'course_title': 'Title override',
'org_logo_path': '/c4x/test/CSS101/asset/org_logo.png',
'signatories': [],
'is_active': True
}
......
......@@ -177,8 +177,6 @@ class CertificateManager(object):
"name": certificate_data['name'],
"description": certificate_data['description'],
"version": CERTIFICATE_SCHEMA_VERSION,
"org_logo_path": certificate_data.get('org_logo_path', ''),
"is_active": certificate_data.get('is_active', False),
"signatories": certificate_data['signatories']
}
......@@ -231,7 +229,6 @@ class CertificateManager(object):
if int(cert['id']) == int(certificate_id):
certificate = course.certificates['certificates'][index]
# Remove any signatory assets prior to dropping the entire cert record from the course
_delete_asset(course.id, certificate['org_logo_path'])
for sig_index, signatory in enumerate(certificate.get('signatories')): # pylint: disable=unused-variable
_delete_asset(course.id, signatory['signature_image_path'])
# Now drop the certificate record
......
......@@ -93,13 +93,11 @@ class HelperMethods(object):
'id': i,
'name': 'Name ' + str(i),
'description': 'Description ' + str(i),
'org_logo_path': '/c4x/test/CSS101/asset/org_logo{}.png'.format(i),
'signatories': signatories,
'version': CERTIFICATE_SCHEMA_VERSION,
'is_active': is_active
} for i in xrange(0, count)
]
self._create_fake_images([certificate['org_logo_path'] for certificate in certificates])
self.course.certificates = {'certificates': certificates}
self.save_course()
......@@ -219,8 +217,6 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat
u'version': CERTIFICATE_SCHEMA_VERSION,
u'name': u'Test certificate',
u'description': u'Test description',
u'org_logo_path': '',
u'is_active': False,
u'signatories': []
}
response = self.client.ajax_post(
......@@ -388,8 +384,6 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific
u'name': u'Test certificate',
u'description': u'Test description',
u'course_title': u'Course Title Override',
u'org_logo_path': '',
u'is_active': False,
u'signatories': []
}
......@@ -420,8 +414,6 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific
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': []
}
......@@ -453,11 +445,6 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific
Delete certificate
"""
self._add_course_certificates(count=2, signatory_count=1)
certificates = self.course.certificates['certificates']
org_logo_url = certificates[1]['org_logo_path']
image_asset_location = AssetLocation.from_deprecated_string(org_logo_url)
content = contentstore().find(image_asset_location)
self.assertIsNotNone(content)
response = self.client.delete(
self._url(cid=1),
content_type="application/json",
......@@ -474,8 +461,6 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific
# Verify that certificates are properly updated in the course.
certificates = self.course.certificates['certificates']
self.assertEqual(len(certificates), 1)
# make sure certificate org logo is deleted too
self.assertRaises(NotFoundError, contentstore().find, image_asset_location)
self.assertEqual(certificates[0].get('name'), 'Name 0')
self.assertEqual(certificates[0].get('description'), 'Description 0')
......
......@@ -20,7 +20,6 @@ function (_, str, Backbone, BackboneRelational, BackboneAssociations, gettext, C
defaults: {
// Metadata fields currently displayed in web forms
course_title: '',
org_logo_path: '',
// Metadata fields not currently displayed in web forms
name: 'Name of the certificate',
......
......@@ -41,7 +41,6 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce
uploadDialog: 'form.upload-dialog',
uploadDialogButton: '.action-upload',
uploadDialogFileInput: 'form.upload-dialog input[type=file]',
uploadOrgLogoButton: '.action-upload-org-logo',
saveCertificateButton: 'button.action-primary'
};
......@@ -311,9 +310,6 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce
setValuesToInputs(this.view, {
inputCertificateDescription: 'New Test Description'
});
this.view.$(SELECTORS.uploadOrgLogoButton).click();
var org_logo_path = '/c4x/edX/DemoX/asset/org-logo.png';
uploadFile(org_logo_path, requests);
setValuesToInputs(this.view, {
inputSignatoryName: 'New Signatory Name'
......@@ -334,8 +330,7 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce
ViewHelpers.submitAndVerifyFormSuccess(this.view, requests, notificationSpy);
expect(this.model).toBeCorrectValuesInModel({
name: 'New Test Name',
description: 'New Test Description',
org_logo_path: org_logo_path
description: 'New Test Description'
});
// get the first signatory from the signatories collection.
......
......@@ -7,12 +7,10 @@ define([ // jshint ignore:line
'gettext',
'js/views/list_item_editor',
'js/certificates/models/signatory',
'js/certificates/views/signatory_editor',
'js/models/uploads',
'js/views/uploads'
'js/certificates/views/signatory_editor'
],
function($, _, Backbone, gettext,
ListItemEditorView, SignatoryModel, SignatoryEditorView, FileUploadModel, FileUploadDialog) {
ListItemEditorView, SignatoryModel, SignatoryEditorView) {
'use strict';
var MAX_SIGNATORIES_LIMIT = 4;
var CertificateEditorView = ListItemEditorView.extend({
......@@ -21,13 +19,11 @@ function($, _, Backbone, gettext,
'change .collection-name-input': 'setName',
'change .certificate-description-input': 'setDescription',
'change .certificate-course-title-input': 'setCourseTitle',
'change .org-logo-input': 'setOrgLogoPath',
'focus .input-text': 'onFocus',
'blur .input-text': 'onBlur',
'submit': 'setAndClose',
'click .action-cancel': 'cancel',
'click .action-add-signatory': 'addSignatory',
'click .action-upload-org-logo': 'uploadLogoImage'
'click .action-add-signatory': 'addSignatory'
},
className: function () {
......@@ -141,45 +137,12 @@ function($, _, Backbone, gettext,
);
},
setOrgLogoPath: function(event) {
// Updates the indicated model field (still requires persistence on server)
if (event && event.preventDefault) { event.preventDefault(); }
var org_logo_path = this.$('.org-logo-input').val();
this.model.set(
'org_logo_path', org_logo_path,
{ silent: true }
);
this.$('.current-org-logo img.org-logo').attr('src', org_logo_path);
},
setValues: function() {
// Update the specified values in the local model instance
this.setName();
this.setDescription();
this.setCourseTitle();
this.setOrgLogoPath();
return this;
},
uploadLogoImage: function(event) {
event.preventDefault();
var upload = new FileUploadModel({
title: gettext("Upload organization logo."),
message: gettext("Maximum logo height should be 125px."),
mimeTypes: ['image/png', 'image/jpeg']
});
var self = this;
var modal = new FileUploadDialog({
model: upload,
onSuccess: function(response) {
var org_logo_path = response.asset.url;
self.model.set('org_logo_path', org_logo_path);
self.$('.current-org-logo img.org-logo').attr('src', org_logo_path);
self.$('.current-org-logo').show();
self.$('input.org-logo-input').attr('value', org_logo_path);
}
});
modal.show();
}
});
return CertificateEditorView;
......
......@@ -313,49 +313,6 @@
border-color: $red;
}
}
.org-logo-upload-wrapper {
@include clearfix();
width: flex-grid(12,12);
.org-logo-upload-input-wrapper {
float: left;
width: flex-grid(7,12);
margin-right: flex-gutter();
}
.action-upload-org-logo {
@extend %ui-btn-flat-outline;
float: right;
width: flex-grid(4,12);
margin-top: ($baseline/4);
padding: ($baseline/2) $baseline;
}
}
.current-org-logo {
margin-bottom: ($baseline/2);
padding: ($baseline/2) $baseline;
background: $gray-l5;
text-align: center;
.wrapper-org-logo {
display: block;
width: 375px;
height: 200px;
overflow: hidden;
margin: 0 auto;
border: 1px solid $gray-l4;
box-shadow: 0 1px 1px $shadow-l1;
padding: ($baseline/2);
background: $white;
}
.org-logo {
display: block;
width: 100%;
min-height: 100%;
}
}
}
label.required {
......
......@@ -24,17 +24,6 @@
<input id="certificate-course-title-<%= uniqueId %>" class="certificate-course-title-input input-text" name="certificate-course-title" type="text" placeholder="<%= gettext("Course title") %>" value="<%= course_title %>" aria-describedby="certificate-course-title-<%=uniqueId %>-tip" />
<span id="certificate-course-title-<%= uniqueId %>-tip" class="tip tip-stacked"><%= gettext("Specify an alternative to the official course title to display on certificates. Leave blank to use the official course title.") %></span>
</div>
<div class="input-wrap field text add-org-logo">
<label for="certificate-org-logo-<%= uniqueId %>"><%= gettext("Organization Logo") %></label>
<div class="current-org-logo" <% if (org_logo_path == "") { print('style="display:none"'); } %>><span class="wrapper-org-logo"><img class="org-logo" src="<%= org_logo_path %>" alt="Organization Logo"></span></div>
<div class="org-logo-upload-wrapper">
<div class="org-logo-upload-input-wrapper">
<input id="certificate-org-logo-<%= uniqueId %>" class="collection-name-input input-text org-logo-input" name="org-logo-path" type="text" placeholder="<%= gettext("Path to organization logo") %>" value="<%= org_logo_path %>" aria-describedby="certificate-org-logo-<%=uniqueId %>-tip" readonly />
<span id="certificate-org-logo-<%=uniqueId %>-tip" class="tip tip-stacked"><%= gettext("Maximum logo height 125px, width variable") %></span>
</div>
<button type="button" class="action action-upload-org-logo">Upload Organization Logo</button>
</div>
</div>
</fieldset>
<header>
<span class="title"><%= gettext("Certificate Signatories") %></legend>
......
# pylint: disable=invalid-name
"""
Utility library for working with the edx-organizations app
"""
from django.conf import settings
def add_organization(organization_data):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
return None
from organizations import api as organizations_api
return organizations_api.add_organization(organization_data=organization_data)
def add_organization_course(organization_data, course_id):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
return None
from organizations import api as organizations_api
return organizations_api.add_organization_course(organization_data=organization_data, course_key=course_id)
def get_organization(organization_id):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
return []
from organizations import api as organizations_api
return organizations_api.get_organization(organization_id)
def get_organizations():
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
return []
from organizations import api as organizations_api
return organizations_api.get_organizations()
def get_organization_courses(organization_id):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
return []
from organizations import api as organizations_api
return organizations_api.get_organization_courses(organization_id)
def get_course_organizations(course_id):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
return []
from organizations import api as organizations_api
return organizations_api.get_course_organizations(course_id)
"""
Tests for the organizations helpers library, which is the integration point for the edx-organizations API
"""
from mock import patch
from util import organizations_helpers
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': False})
class OrganizationsHelpersTestCase(ModuleStoreTestCase):
"""
Main test suite for Organizations API client library
"""
def setUp(self):
"""
Test case scaffolding
"""
super(OrganizationsHelpersTestCase, self).setUp(create_user=False)
self.course = CourseFactory.create()
self.organization = {
'name': 'Test Organization',
'description': 'Testing Organization Helpers Library',
}
def test_get_organization_returns_none_when_app_disabled(self):
response = organizations_helpers.get_organization(1)
self.assertEqual(len(response), 0)
def test_get_organizations_returns_none_when_app_disabled(self):
response = organizations_helpers.get_organizations()
self.assertEqual(len(response), 0)
def test_get_organization_courses_returns_none_when_app_disabled(self):
response = organizations_helpers.get_organization_courses(1)
self.assertEqual(len(response), 0)
def test_get_course_organizations_returns_none_when_app_disabled(self):
response = organizations_helpers.get_course_organizations(unicode(self.course.id))
self.assertEqual(len(response), 0)
def test_add_organization_returns_none_when_app_disabled(self):
response = organizations_helpers.add_organization(organization_data=self.organization)
self.assertIsNone(response)
def test_add_organization_course_returns_none_when_app_disabled(self):
response = organizations_helpers.add_organization_course(self.organization, self.course.id)
self.assertIsNone(response)
......@@ -215,7 +215,6 @@ class GenerateUserCertificatesTest(EventTestMixin, ModuleStoreTestCase):
'name': 'Test Certificate Name',
'description': 'Test Certificate Description',
'course_title': 'tes_course_title',
'org_logo_path': '/t4x/orgX/testX/asset/org-logo.png',
'signatories': [],
'version': 1,
'is_active': True
......
......@@ -37,6 +37,7 @@ from certificates.tests.factories import (
BadgeAssertionFactory,
)
from lms import urls
from util import organizations_helpers as organizations_api
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
......@@ -259,7 +260,6 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
'name': 'Name ' + str(i),
'description': 'Description ' + str(i),
'course_title': 'course_title_' + str(i),
'org_logo_path': '/t4x/orgX/testX/asset/org-logo-{}.png'.format(i),
'signatories': signatories,
'version': 1,
'is_active': is_active
......@@ -428,6 +428,39 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
self.store.update_item(self.course, self.user.id)
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
def test_rendering_course_organization_data(self):
"""
Test: organization data should render on certificate web view if course has organization.
"""
test_organization_data = {
'name': 'test_organization',
'description': 'Test Organization Description',
'active': True,
'logo': '/logo_test1.png/'
}
test_org = organizations_api.add_organization(organization_data=test_organization_data)
organizations_api.add_organization_course(organization_data=test_org, course_id=unicode(self.course.id))
self._add_course_certificates(count=1, signatory_count=1, is_active=True)
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course.id)
)
response = self.client.get(test_url)
self.assertIn(
'a course of study offered by test_organization',
response.content
)
self.assertNotIn(
'a course of study offered by testorg',
response.content
)
self.assertIn(
'<title>test_organization {} Certificate |'.format(self.course.number, ),
response.content
)
self.assertIn('logo_test1.png', response.content)
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
def test_render_html_view_valid_certificate(self):
test_url = get_certificate_url(
user_id=self.user.id,
......@@ -458,7 +491,6 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
self._add_course_certificates(count=1, signatory_count=2)
response = self.client.get(test_url)
self.assertIn('course_title_0', response.content)
self.assertIn('/t4x/orgX/testX/asset/org-logo-0.png', response.content)
self.assertIn('Signatory_Name 0', response.content)
self.assertIn('Signatory_Title 0', response.content)
self.assertIn('Signatory_Organization 0', response.content)
......
......@@ -43,6 +43,7 @@ from student.models import LinkedInAddToProfileConfiguration
from util.json_request import JsonResponse, JsonResponseBadRequest
from util.bad_request_rate_limiter import BadRequestRateLimiter
from courseware.courses import course_image_url
from util import organizations_helpers as organization_api
logger = logging.getLogger(__name__)
......@@ -299,13 +300,20 @@ def _update_certificate_context(context, course, user, user_certificate):
user_fullname = user.profile.name
platform_name = microsite.get_value("platform_name", settings.PLATFORM_NAME)
certificate_type = context.get('certificate_type')
partner_name = course.org
organizations = organization_api.get_course_organizations(course_id=course.id)
if organizations:
#TODO Need to add support for multiple organizations, Currently we are interested in the first one.
organization = organizations[0]
partner_name = organization.get('name', course.org)
context['organization_logo'] = organization.get('logo', None)
context['username'] = user.username
context['course_mode'] = user_certificate.mode
context['accomplishment_user_id'] = user.id
context['accomplishment_copy_name'] = user_fullname
context['accomplishment_copy_username'] = user.username
context['accomplishment_copy_course_org'] = course.org
context['accomplishment_copy_course_org'] = partner_name
context['accomplishment_copy_course_name'] = course.display_name
context['course_image_url'] = course_image_url(course)
context['share_settings'] = settings.FEATURES.get('SOCIAL_SHARING_SETTINGS', {})
......@@ -330,14 +338,10 @@ def _update_certificate_context(context, course, user, user_certificate):
year=user_certificate.modified_date.year
)
accd_course_org_html = '<span class="detail--xuniversity">{partner_name}</span>'.format(partner_name=course.org)
accd_platform_name_html = '<span class="detail--company">{platform_name}</span>'.format(platform_name=platform_name)
# Translators: This line appears on the certificate after the name of a course, and provides more
# information about the organizations providing the course material to platform users
context['accomplishment_copy_course_description'] = _('a course of study offered by {partner_name}, '
'through {platform_name}.').format(
partner_name=accd_course_org_html,
platform_name=accd_platform_name_html
partner_name=partner_name,
platform_name=platform_name
)
# Translators: Accomplishments describe the awards/certifications obtained by students on this platform
......@@ -412,13 +416,13 @@ def _update_certificate_context(context, course, user, user_certificate):
'who participated in {partner_name} {course_number}').format(
platform_name=platform_name,
user_name=user_fullname,
partner_name=course.org,
partner_name=partner_name,
course_number=course.number
)
# Translators: This text is bound to the HTML 'title' element of the page and appears in the browser title bar
context['document_title'] = _("{partner_name} {course_number} Certificate | {platform_name}").format(
partner_name=course.org,
partner_name=partner_name,
course_number=course.number,
platform_name=platform_name
)
......
......@@ -897,7 +897,6 @@ class ProgressPageTests(ModuleStoreTestCase):
'name': 'Name 1',
'description': 'Description 1',
'course_title': 'course_title_1',
'org_logo_path': '/t4x/orgX/testX/asset/org-logo-1.png',
'signatories': [],
'version': 1,
'is_active': True
......
......@@ -346,6 +346,9 @@ FEATURES = {
# Milestones application flag
'MILESTONES_APP': False,
# Organizations application flag
'ORGANIZATIONS_APP': False,
# Prerequisite courses feature flag
'ENABLE_PREREQUISITE_COURSES': False,
......@@ -2402,6 +2405,9 @@ OPTIONAL_APPS = (
# edX Proctoring
'edx_proctoring',
# Organizations App (http://github.com/edx/edx-organizations)
'organizations',
)
for app_name in OPTIONAL_APPS:
......
......@@ -121,6 +121,8 @@ PASSWORD_COMPLEXITY = {}
########################### Milestones #################################
FEATURES['MILESTONES_APP'] = True
########################### Milestones #################################
FEATURES['ORGANIZATIONS_APP'] = True
########################### Entrance Exams #################################
FEATURES['ENTRANCE_EXAMS'] = True
......
......@@ -520,3 +520,6 @@ PROFILE_IMAGE_MIN_BYTES = 100
FEATURES['ENABLE_LTI_PROVIDER'] = True
INSTALLED_APPS += ('lti_provider',)
AUTHENTICATION_BACKENDS += ('lti_provider.users.LtiBackend',)
# ORGANIZATIONS
FEATURES['ORGANIZATIONS_APP'] = True
......@@ -75,10 +75,10 @@ course_mode_class = course_mode if course_mode else ''
</div>
</li>
% if certificate_data and certificate_data.get('org_logo_path', ''):
% if organization_logo:
<li class="wrapper-organization">
<div class="organization">
<img class="organization-logo" src="${static.url(certificate_data['org_logo_path'])}" alt="${platform_name}">
<img class="organization-logo" src="${organization_logo.url}" alt="${platform_name}">
</div>
</li>
% endif
......
......@@ -57,6 +57,7 @@ git+https://github.com/edx/edx-lint.git@ed8c8d2a0267d4d42f43642d193e25f8bd575d9b
git+https://github.com/edx/ecommerce-api-client.git@1.1.0#egg=ecommerce-api-client==1.1.0
-e git+https://github.com/edx/edx-user-state-client.git@30c0ad4b9f57f8d48d6943eb585ec8a9205f4469#egg=edx-user-state-client
-e git+https://github.com/edx/edx-proctoring.git@release-2015-07-29#egg=edx-proctoring
-e git+https://github.com/edx/edx-organizations.git@release-2015-08-03#egg=edx-organizations
# Third Party XBlocks
-e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga
......
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