diff --git a/common/djangoapps/course_modes/tests/factories.py b/common/djangoapps/course_modes/tests/factories.py index 4c32dea10f..0cfb439700 100644 --- a/common/djangoapps/course_modes/tests/factories.py +++ b/common/djangoapps/course_modes/tests/factories.py @@ -1,5 +1,6 @@ from course_modes.models import CourseMode from factory.django import DjangoModelFactory +from xmodule.modulestore.locations import SlashSeparatedCourseKey # Factories don't have __init__ methods, and are self documenting @@ -7,9 +8,10 @@ from factory.django import DjangoModelFactory class CourseModeFactory(DjangoModelFactory): FACTORY_FOR = CourseMode - course_id = u'MITx/999/Robot_Super_Course' + course_id = SlashSeparatedCourseKey('MITx', '999', 'Robot_Super_Course') mode_slug = 'audit' mode_display_name = 'audit course' min_price = 0 currency = 'usd' expiration_datetime = None + suggested_prices = '' diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py new file mode 100644 index 0000000000..9de6f3c09f --- /dev/null +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -0,0 +1,100 @@ +import ddt +import unittest +from django.test import TestCase +from django.conf import settings +from django.core.urlresolvers import reverse +from mock import patch, Mock + +from course_modes.tests.factories import CourseModeFactory +from student.tests.factories import CourseEnrollmentFactory, UserFactory +from xmodule.modulestore.locations import SlashSeparatedCourseKey + + +@ddt.ddt +class CourseModeViewTest(TestCase): + + def setUp(self): + self.course_id = SlashSeparatedCourseKey('org', 'course', 'run') + + for mode in ('audit', 'verified', 'honor'): + CourseModeFactory(mode_slug=mode, course_id=self.course_id) + + @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') + @ddt.data( + # is_active?, enrollment_mode, upgrade?, redirect? + (True, 'verified', True, True), # User is already verified + (True, 'verified', False, True), # User is already verified + (True, 'honor', True, False), # User isn't trying to upgrade + (True, 'honor', False, True), # User is trying to upgrade + (True, 'audit', True, False), # User isn't trying to upgrade + (True, 'audit', False, True), # User is trying to upgrade + (False, 'verified', True, False), # User isn't active + (False, 'verified', False, False), # User isn't active + (False, 'honor', True, False), # User isn't active + (False, 'honor', False, False), # User isn't active + (False, 'audit', True, False), # User isn't active + (False, 'audit', False, False), # User isn't active + ) + @ddt.unpack + @patch('course_modes.views.modulestore', Mock()) + def test_reregister_redirect(self, is_active, enrollment_mode, upgrade, redirect): + enrollment = CourseEnrollmentFactory( + is_active=is_active, + mode=enrollment_mode, + course_id=self.course_id + ) + + self.client.login( + username=enrollment.user.username, + password='test' + ) + + if upgrade: + get_params = {'upgrade': True} + else: + get_params = {} + + response = self.client.get( + reverse('course_modes_choose', args=[self.course_id.to_deprecated_string()]), + get_params, + follow=False, + ) + + if redirect: + self.assertEquals(response.status_code, 302) + self.assertTrue(response['Location'].endswith(reverse('dashboard'))) + else: + self.assertEquals(response.status_code, 200) + # TODO: Fix it so that response.templates works w/ mako templates, and then assert + # that the right template rendered + + @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') + @ddt.data( + '', + '1,,2', + '1, ,2', + '1, 2, 3' + ) + @patch('course_modes.views.modulestore', Mock()) + def test_suggested_prices(self, price_list): + course_id = SlashSeparatedCourseKey('org', 'course', 'price_course') + user = UserFactory() + + for mode in ('audit', 'honor'): + CourseModeFactory(mode_slug=mode, course_id=course_id) + + CourseModeFactory(mode_slug='verified', course_id=course_id, suggested_prices=price_list) + + self.client.login( + username=user.username, + password='test' + ) + + response = self.client.get( + reverse('course_modes_choose', args=[self.course_id.to_deprecated_string()]), + follow=False, + ) + + self.assertEquals(response.status_code, 200) + # TODO: Fix it so that response.templates works w/ mako templates, and then assert + # that the right template rendered diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 68b44e61cb..62855f65f6 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -36,16 +36,14 @@ class ChooseModeView(View): course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) - enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_key) + enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key) upgrade = request.GET.get('upgrade', False) request.session['attempting_upgrade'] = upgrade + # Inactive users always need to re-register # verified users do not need to register or upgrade - if enrollment_mode == 'verified': - return redirect(reverse('dashboard')) - # registered users who are not trying to upgrade do not need to re-register - if enrollment_mode is not None and upgrade is False: + if is_active and (upgrade is False or enrollment_mode == 'verified'): return redirect(reverse('dashboard')) modes = CourseMode.modes_for_course_dict(course_key) @@ -64,7 +62,11 @@ class ChooseModeView(View): "upgrade": upgrade, } if "verified" in modes: - context["suggested_prices"] = [decimal.Decimal(x) for x in modes["verified"].suggested_prices.split(",")] + context["suggested_prices"] = [ + decimal.Decimal(x.strip()) + for x in modes["verified"].suggested_prices.split(",") + if x.strip() + ] context["currency"] = modes["verified"].currency.upper() context["min_price"] = modes["verified"].min_price diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 6faec29c0e..8654a32110 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -880,18 +880,15 @@ class CourseEnrollment(models.Model): `user` is a Django User object `course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall) - Returns the mode for both inactive and active users. - Returns None if the courseenrollment record does not exist. + Returns (mode, is_active) where mode is the enrollment mode of the student + and is_active is whether the enrollment is active. + Returns (None, None) if the courseenrollment record does not exist. """ try: record = CourseEnrollment.objects.get(user=user, course_id=course_id) - - if hasattr(record, 'mode'): - return record.mode - else: - return None + return (record.mode, record.is_active) except cls.DoesNotExist: - return None + return (None, None) @classmethod def enrollments_for_user(cls, user): diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py index d44ee95b80..6ad1962c4e 100644 --- a/common/djangoapps/student/tests/factories.py +++ b/common/djangoapps/student/tests/factories.py @@ -10,6 +10,7 @@ import factory from factory.django import DjangoModelFactory from uuid import uuid4 from pytz import UTC +from xmodule.modulestore.locations import SlashSeparatedCourseKey # Factories don't have __init__ methods, and are self documenting # pylint: disable=W0232, C0111 @@ -109,14 +110,14 @@ class CourseEnrollmentFactory(DjangoModelFactory): FACTORY_FOR = CourseEnrollment user = factory.SubFactory(UserFactory) - course_id = u'edX/toy/2012_Fall' + course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') class CourseEnrollmentAllowedFactory(DjangoModelFactory): FACTORY_FOR = CourseEnrollmentAllowed email = 'test@edx.org' - course_id = 'edX/test/2012_Fall' + course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') class PendingEmailChangeFactory(DjangoModelFactory): diff --git a/common/lib/xmodule/xmodule/annotator_mixin.py b/common/lib/xmodule/xmodule/annotator_mixin.py index aa597702db..aa0a19d4a8 100644 --- a/common/lib/xmodule/xmodule/annotator_mixin.py +++ b/common/lib/xmodule/xmodule/annotator_mixin.py @@ -7,7 +7,6 @@ from urlparse import urlparse from os.path import splitext, basename from HTMLParser import HTMLParser - def get_instructions(xmltree): """ Removes from the xmltree and returns them as a string, otherwise None. """ instructions = xmltree.find('instructions') diff --git a/common/lib/xmodule/xmodule/imageannotation_module.py b/common/lib/xmodule/xmodule/imageannotation_module.py index 9d5ab94083..7a37b35334 100644 --- a/common/lib/xmodule/xmodule/imageannotation_module.py +++ b/common/lib/xmodule/xmodule/imageannotation_module.py @@ -9,13 +9,19 @@ from xmodule.raw_module import RawDescriptor from xblock.core import Scope, String from xmodule.annotator_mixin import get_instructions, html_to_text from xmodule.annotator_token import retrieve_token +from xblock.fragment import Fragment import textwrap +# Make '_' a no-op so we can scrape strings +_ = lambda text: text + class AnnotatableFields(object): """ Fields for `ImageModule` and `ImageDescriptor`. """ - data = String(help="XML data for the annotation", scope=Scope.content, default=textwrap.dedent("""\ + data = String(help=_("XML data for the annotation"), + scope=Scope.content, + default=textwrap.dedent("""\

@@ -36,28 +42,47 @@ class AnnotatableFields(object): """)) display_name = String( - display_name="Display Name", - help="Display name for this module", + display_name=_("Display Name"), + help=_("Display name for this module"), scope=Scope.settings, default='Image Annotation', ) instructor_tags = String( - display_name="Tags for Assignments", - help="Add tags that automatically highlight in a certain color using the comma-separated form, i.e. imagery:red,parallelism:blue", + display_name=_("Tags for Assignments"), + help=_("Add tags that automatically highlight in a certain color using the comma-separated form, i.e. imagery:red,parallelism:blue"), scope=Scope.settings, default='professor:green,teachingAssistant:blue', ) annotation_storage_url = String( - help="Location of Annotation backend", + help=_("Location of Annotation backend"), scope=Scope.settings, default="http://your_annotation_storage.com", - display_name="Url for Annotation Storage" + display_name=_("Url for Annotation Storage") ) annotation_token_secret = String( - help="Secret string for annotation storage", + help=_("Secret string for annotation storage"), scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - display_name="Secret Token String for Annotation" + display_name=_("Secret Token String for Annotation") + ) + default_tab = String( + display_name=_("Default Annotations Tab"), + help=_("Select which tab will be the default in the annotations table: myNotes, Instructor, or Public."), + scope=Scope.settings, + default="myNotes", + ) + # currently only supports one instructor, will build functionality for multiple later + instructor_email = String( + display_name=_("Email for 'Instructor' Annotations"), + help=_("Email of the user that will be attached to all annotations that will be found in 'Instructor' tab."), + scope=Scope.settings, + default="", + ) + annotation_mode = String( + display_name=_("Mode for Annotation Tool"), + help=_("Type in number corresponding to following modes: 'instructor' or 'everyone'"), + scope=Scope.settings, + default="everyone", ) @@ -91,18 +116,23 @@ class ImageAnnotationModule(AnnotatableFields, XModule): """ Removes from the xmltree and returns them as a string, otherwise None. """ return get_instructions(xmltree) - def get_html(self): + def student_view(self, context): """ Renders parameters to template. """ context = { 'display_name': self.display_name_with_default, 'instructions_html': self.instructions, - 'annotation_storage': self.annotation_storage_url, 'token': retrieve_token(self.user, self.annotation_token_secret), 'tag': self.instructor_tags, 'openseadragonjson': self.openseadragonjson, + 'annotation_storage': self.annotation_storage_url, + 'default_tab': self.default_tab, + 'instructor_email': self.instructor_email, + 'annotation_mode': self.annotation_mode, } - - return self.system.render_template('imageannotation.html', context) + fragment = Fragment(self.system.render_template('imageannotation.html', context)) + fragment.add_javascript_url("/static/js/vendor/tinymce/js/tinymce/tinymce.full.min.js") + fragment.add_javascript_url("/static/js/vendor/tinymce/js/tinymce/jquery.tinymce.min.js") + return fragment class ImageAnnotationDescriptor(AnnotatableFields, RawDescriptor): # pylint: disable=abstract-method diff --git a/common/lib/xmodule/xmodule/split_test_module.py b/common/lib/xmodule/xmodule/split_test_module.py index 40a772bcf2..15845978fb 100644 --- a/common/lib/xmodule/xmodule/split_test_module.py +++ b/common/lib/xmodule/xmodule/split_test_module.py @@ -226,7 +226,7 @@ class SplitTestModule(SplitTestFields, XModule): Record in the tracking logs which child was rendered """ # TODO: use publish instead, when publish is wired to the tracking logs - self.system.track_function('xblock.split_test.child_render', {'child-id': self.child.scope_ids.usage_id}) + self.system.track_function('xblock.split_test.child_render', {'child-id': self.child.scope_ids.usage_id.to_deprecated_string()}) return Response() def get_icon_class(self): diff --git a/common/lib/xmodule/xmodule/tests/test_imageannotation.py b/common/lib/xmodule/xmodule/tests/test_imageannotation.py index c19ffc25d0..531d4784ac 100644 --- a/common/lib/xmodule/xmodule/tests/test_imageannotation.py +++ b/common/lib/xmodule/xmodule/tests/test_imageannotation.py @@ -69,10 +69,10 @@ class ImageAnnotationModuleTestCase(unittest.TestCase): actual = self.mod._extract_instructions(xmltree) # pylint: disable=protected-access self.assertIsNone(actual) - def test_get_html(self): + def test_student_view(self): """ Tests the function that passes in all the information in the context that will be used in templates/textannotation.html """ - context = self.mod.get_html() + context = self.mod.student_view({}).content for key in ['display_name', 'instructions_html', 'annotation_storage', 'token', 'tag', 'openseadragonjson']: self.assertIn(key, context) diff --git a/common/lib/xmodule/xmodule/tests/test_textannotation.py b/common/lib/xmodule/xmodule/tests/test_textannotation.py index 907eb78780..31d420dd08 100644 --- a/common/lib/xmodule/xmodule/tests/test_textannotation.py +++ b/common/lib/xmodule/xmodule/tests/test_textannotation.py @@ -54,10 +54,10 @@ class TextAnnotationModuleTestCase(unittest.TestCase): actual = self.mod._extract_instructions(xmltree) # pylint: disable=W0212 self.assertIsNone(actual) - def test_get_html(self): + def test_student_view(self): """ Tests the function that passes in all the information in the context that will be used in templates/textannotation.html """ - context = self.mod.get_html() + context = self.mod.student_view({}).content for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage', 'token']: self.assertIn(key, context) diff --git a/common/lib/xmodule/xmodule/tests/test_videoannotation.py b/common/lib/xmodule/xmodule/tests/test_videoannotation.py index 410d39d4b1..0dc1a5d902 100644 --- a/common/lib/xmodule/xmodule/tests/test_videoannotation.py +++ b/common/lib/xmodule/xmodule/tests/test_videoannotation.py @@ -62,10 +62,10 @@ class VideoAnnotationModuleTestCase(unittest.TestCase): self.assertEqual(expectedyoutube, result2) self.assertEqual(expectednotyoutube, result1) - def test_get_html(self): + def test_student_view(self): """ Tests to make sure variables passed in truly exist within the html once it is all rendered. """ - context = self.mod.get_html() + context = self.mod.student_view({}).content for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage']: self.assertIn(key, context) diff --git a/common/lib/xmodule/xmodule/textannotation_module.py b/common/lib/xmodule/xmodule/textannotation_module.py index 258f270787..35b1fd8c96 100644 --- a/common/lib/xmodule/xmodule/textannotation_module.py +++ b/common/lib/xmodule/xmodule/textannotation_module.py @@ -8,7 +8,7 @@ from xmodule.raw_module import RawDescriptor from xblock.core import Scope, String from xmodule.annotator_mixin import get_instructions from xmodule.annotator_token import retrieve_token - +from xblock.fragment import Fragment import textwrap # Make '_' a no-op so we can scrape strings @@ -17,7 +17,9 @@ _ = lambda text: text class AnnotatableFields(object): """Fields for `TextModule` and `TextDescriptor`.""" - data = String(help=_("XML data for the annotation"), scope=Scope.content, default=textwrap.dedent("""\ + data = String(help=_("XML data for the annotation"), + scope=Scope.content, + default=textwrap.dedent("""\

@@ -47,8 +49,43 @@ class AnnotatableFields(object): scope=Scope.settings, default='None', ) - annotation_storage_url = String(help=_("Location of Annotation backend"), scope=Scope.settings, default="http://your_annotation_storage.com", display_name=_("Url for Annotation Storage")) - annotation_token_secret = String(help=_("Secret string for annotation storage"), scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name=_("Secret Token String for Annotation")) + diacritics = String( + display_name=_("Diacritic Marks"), + help=_("Add diacritic marks to be added to a text using the comma-separated form, i.e. markname;urltomark;baseline,markname2;urltomark2;baseline2"), + scope=Scope.settings, + default='', + ) + annotation_storage_url = String( + help=_("Location of Annotation backend"), + scope=Scope.settings, + default="http://your_annotation_storage.com", + display_name=_("Url for Annotation Storage") + ) + annotation_token_secret = String( + help=_("Secret string for annotation storage"), + scope=Scope.settings, + default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + display_name=_("Secret Token String for Annotation") + ) + default_tab = String( + display_name=_("Default Annotations Tab"), + help=_("Select which tab will be the default in the annotations table: myNotes, Instructor, or Public."), + scope=Scope.settings, + default="myNotes", + ) + # currently only supports one instructor, will build functionality for multiple later + instructor_email = String( + display_name=_("Email for 'Instructor' Annotations"), + help=_("Email of the user that will be attached to all annotations that will be found in 'Instructor' tab."), + scope=Scope.settings, + default="", + ) + annotation_mode = String( + display_name=_("Mode for Annotation Tool"), + help=_("Type in number corresponding to following modes: 'instructor' or 'everyone'"), + scope=Scope.settings, + default="everyone", + ) class TextAnnotationModule(AnnotatableFields, XModule): @@ -73,7 +110,7 @@ class TextAnnotationModule(AnnotatableFields, XModule): """ Removes from the xmltree and returns them as a string, otherwise None. """ return get_instructions(xmltree) - def get_html(self): + def student_view(self, context): """ Renders parameters to template. """ context = { 'course_key': self.runtime.course_id, @@ -82,10 +119,17 @@ class TextAnnotationModule(AnnotatableFields, XModule): 'source': self.source, 'instructions_html': self.instructions, 'content_html': self.content, - 'annotation_storage': self.annotation_storage_url, 'token': retrieve_token(self.user_email, self.annotation_token_secret), + 'diacritic_marks': self.diacritics, + 'annotation_storage': self.annotation_storage_url, + 'default_tab': self.default_tab, + 'instructor_email': self.instructor_email, + 'annotation_mode': self.annotation_mode, } - return self.system.render_template('textannotation.html', context) + fragment = Fragment(self.system.render_template('textannotation.html', context)) + fragment.add_javascript_url("/static/js/vendor/tinymce/js/tinymce/tinymce.full.min.js") + fragment.add_javascript_url("/static/js/vendor/tinymce/js/tinymce/jquery.tinymce.min.js") + return fragment class TextAnnotationDescriptor(AnnotatableFields, RawDescriptor): diff --git a/common/lib/xmodule/xmodule/videoannotation_module.py b/common/lib/xmodule/xmodule/videoannotation_module.py index f5bd7d3e93..6a8584505b 100644 --- a/common/lib/xmodule/xmodule/videoannotation_module.py +++ b/common/lib/xmodule/xmodule/videoannotation_module.py @@ -9,6 +9,7 @@ from xmodule.raw_module import RawDescriptor from xblock.core import Scope, String from xmodule.annotator_mixin import get_instructions, get_extension from xmodule.annotator_token import retrieve_token +from xblock.fragment import Fragment import textwrap @@ -18,7 +19,9 @@ _ = lambda text: text class AnnotatableFields(object): """ Fields for `VideoModule` and `VideoDescriptor`. """ - data = String(help=_("XML data for the annotation"), scope=Scope.content, default=textwrap.dedent("""\ + data = String(help=_("XML data for the annotation"), + scope=Scope.content, + default=textwrap.dedent("""\

@@ -31,12 +34,51 @@ class AnnotatableFields(object): display_name=_("Display Name"), help=_("Display name for this module"), scope=Scope.settings, - default='Video Annotation', + default=_('Video Annotation'), ) - sourceurl = String(help=_("The external source URL for the video."), display_name=_("Source URL"), scope=Scope.settings, default="http://video-js.zencoder.com/oceans-clip.mp4") - poster_url = String(help=_("Poster Image URL"), display_name=_("Poster URL"), scope=Scope.settings, default="") - annotation_storage_url = String(help=_("Location of Annotation backend"), scope=Scope.settings, default="http://your_annotation_storage.com", display_name=_("Url for Annotation Storage")) - annotation_token_secret = String(help=_("Secret string for annotation storage"), scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name=_("Secret Token String for Annotation")) + sourceurl = String( + help=_("The external source URL for the video."), + display_name=_("Source URL"), + scope=Scope.settings, default="http://video-js.zencoder.com/oceans-clip.mp4" + ) + poster_url = String( + help=_("Poster Image URL"), + display_name=_("Poster URL"), + scope=Scope.settings, + default="" + ) + annotation_storage_url = String( + help=_("Location of Annotation backend"), + scope=Scope.settings, + default="http://your_annotation_storage.com", + display_name=_("Url for Annotation Storage") + ) + annotation_token_secret = String( + help=_("Secret string for annotation storage"), + scope=Scope.settings, + default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + display_name=_("Secret Token String for Annotation") + ) + default_tab = String( + display_name=_("Default Annotations Tab"), + help=_("Select which tab will be the default in the annotations table: myNotes, Instructor, or Public."), + scope=Scope.settings, + default="myNotes", + ) + # currently only supports one instructor, will build functionality for multiple later + instructor_email = String( + display_name=_("Email for 'Instructor' Annotations"), + help=_("Email of the user that will be attached to all annotations that will be found in 'Instructor' tab."), + scope=Scope.settings, + default="", + ) + annotation_mode = String( + display_name=_("Mode for Annotation Tool"), + help=_("Type in number corresponding to following modes: 'instructor' or 'everyone'"), + scope=Scope.settings, + default="everyone", + ) + class VideoAnnotationModule(AnnotatableFields, XModule): '''Video Annotation Module''' @@ -72,7 +114,7 @@ class VideoAnnotationModule(AnnotatableFields, XModule): ''' get the extension of a given url ''' return get_extension(src_url) - def get_html(self): + def student_view(self, context): """ Renders parameters to template. """ extension = self._get_extension(self.sourceurl) @@ -84,11 +126,16 @@ class VideoAnnotationModule(AnnotatableFields, XModule): 'typeSource': extension, 'poster': self.poster_url, 'content_html': self.content, - 'annotation_storage': self.annotation_storage_url, 'token': retrieve_token(self.user_email, self.annotation_token_secret), + 'annotation_storage': self.annotation_storage_url, + 'default_tab': self.default_tab, + 'instructor_email': self.instructor_email, + 'annotation_mode': self.annotation_mode, } - - return self.system.render_template('videoannotation.html', context) + fragment = Fragment(self.system.render_template('videoannotation.html', context)) + fragment.add_javascript_url("/static/js/vendor/tinymce/js/tinymce/tinymce.full.min.js") + fragment.add_javascript_url("/static/js/vendor/tinymce/js/tinymce/jquery.tinymce.min.js") + return fragment class VideoAnnotationDescriptor(AnnotatableFields, RawDescriptor): diff --git a/common/static/js/vendor/ova/OpenSeaDragonAnnotation.js b/common/static/js/vendor/ova/OpenSeaDragonAnnotation.js index d8c2f595c0..938af6e8d3 100644 --- a/common/static/js/vendor/ova/OpenSeaDragonAnnotation.js +++ b/common/static/js/vendor/ova/OpenSeaDragonAnnotation.js @@ -380,19 +380,20 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. clickTimeThreshold: viewer.clickTimeThreshold, clickDistThreshold: viewer.clickDistThreshold }); - - /* Set elements to the control menu */ - viewer.annotatorControl = viewer.wrapperAnnotation.element; - if( viewer.toolbar ){ - viewer.toolbar.addControl( - viewer.annotatorControl, - {anchor: $.ControlAnchor.BOTTOM_RIGHT} - ); - }else{ - viewer.addControl( - viewer.annotatorControl, - {anchor: $.ControlAnchor.TOP_LEFT} - ); + if(this.options.viewer.annotation_mode == "everyone" || this.options.viewer.flags){ + /* Set elements to the control menu */ + viewer.annotatorControl = viewer.wrapperAnnotation.element; + if( viewer.toolbar ){ + viewer.toolbar.addControl( + viewer.annotatorControl, + {anchor: $.ControlAnchor.BOTTOM_RIGHT} + ); + }else{ + viewer.addControl( + viewer.annotatorControl, + {anchor: $.ControlAnchor.TOP_LEFT} + ); + } } }, _reset: function(){ diff --git a/common/static/js/vendor/ova/catch/css/main.css b/common/static/js/vendor/ova/catch/css/main.css index b79bd80e0a..5f5406c560 100644 --- a/common/static/js/vendor/ova/catch/css/main.css +++ b/common/static/js/vendor/ova/catch/css/main.css @@ -379,14 +379,14 @@ display:inline-block; color:#302f2f; font-family:arial; - font-size:15px; + font-size:14px; font-weight:bold; padding:6px 24px; text-decoration:none; margin: 0px 0px 10px 0px; cursor:pointer; - width:140px; + width:115px; text-align:center; } @@ -468,7 +468,7 @@ #mainCatch .searchbox input{ margin: 0; padding: 0; - width: 60%; + width: 50%; margin-left: 10px; display: inline; float: left; @@ -493,19 +493,28 @@ cursor:pointer; } +#mainCatch .searchbox .clear-search-icon{ + font-size: 12px; + text-decoration: underline; + float: right; + margin-top: 10px; + padding-right: 3px; + cursor:pointer; +} + #mainCatch .searchbox .search-icon:hover{ opacity:0.5; box-shadow: 2px 4px 5px #888888; } #mainCatch .selectors{ - width:40%; + width:45%; position:relative; float:left; } #mainCatch .searchbox{ - width:60%; + width:52%; position:relative; float:right; } @@ -515,6 +524,7 @@ position:relative; padding-right:5px; margin-top:8px; + font-size:14px; } #mainCatch .replies .replyItem .deleteReply{ diff --git a/common/static/js/vendor/ova/catch/js/catch.js b/common/static/js/vendor/ova/catch/js/catch.js index 442873659f..a45b778d99 100644 --- a/common/static/js/vendor/ova/catch/js/catch.js +++ b/common/static/js/vendor/ova/catch/js/catch.js @@ -42,29 +42,29 @@ annotationList: ''+ '

'+ - 'User'+ + gettext('User')+ '
'+ '
'+ - 'Annotation'+ + gettext('Annotation')+ '
'+ '{{#if videoFormat}}'+ '
'+ - 'Start'+ + gettext('Start')+ '
'+ '
'+ - 'End'+ + gettext('End')+ '
'+ '{{/if}}'+ '
'+ - '#Replies'+ + gettext('#Replies')+ '
'+ '
'+ - 'Date posted'+ + gettext('Date posted')+ '
'+ ''+ ''+ @@ -73,30 +73,41 @@ annotationList: '{{/each}}'+ ''+ '
'+ - '
More
'+ + '
'+gettext('More')+'
'+ '
', +//Main->PublicPrivateInstructor +annotationPublicPrivateInstructor: + '
'+gettext('My Notes')+'myNotes
'+ + '
'+gettext('Instructor')+'instructor
'+ + '
'+gettext('Public')+'public
'+ + '', + //Main->PublicPrivate annotationPublicPrivate: - '
My NotesmyNotes
'+ - '
Publicpublic
'+ - '', + '
'+gettext('My Notes')+'myNotes
'+ + '
'+gettext('Public')+'public
'+ + '', //Main->MediaSelector annotationMediaSelector: '
    '+ '
  • '+ - 'Text'+ + gettext('Text')+ '
  • '+ '
  • '+ - 'Video'+ + gettext('Video')+ '
  • '+ - 'li class="ui-state-default" media="image">'+ - 'Image'+ + '
  • '+ + gettext('Image')+ '
  • '+ '
', @@ -126,7 +137,7 @@ annotationReply: '
'+ ''+ '{{/if}}'+ - '
Delete
'+ + '
'+gettext('Delete')+'
'+ '

'+ '

'+ '{{#if this.text}}'+ @@ -233,13 +244,13 @@ annotationDetail: ''+ '

'+ - '
Reply
 '+ + '
'+gettext('Reply')+'
 '+ '
Show Replies
 '+ '{{#if authToEditButton}}'+ - '
Edit
'+ + '
'+gettext('Edit')+'
'+ '{{/if}}'+ '{{#if authToDeleteButton}}'+ - '
Delete
'+ + '
'+gettext('Delete')+'
'+ '{{/if}}'+ '
'+ @@ -248,7 +259,7 @@ annotationDetail: '{{#if tags}}'+ '
'+ - '

Tags:

'+ + '

'+gettext('Tags:')+'

'+ '{{#each tags}}'+ '
'+ '{{this}}'+ @@ -290,14 +301,14 @@ CatchAnnotation = function (element, options) { //Reset element an create a new element div element.html('
'); - + this.current_tab = this.options.default_tab; //INIT var self = this; $( document ).ready(function() { self.init(); self.refreshCatch(true); - var moreBut = self.element.find('.annotationListButtons .moreButtonCatch'); - moreBut.hide(); + var moreBut = self.element.find('.annotationListButtons .moreButtonCatch'); + moreBut.hide(); }); return this; @@ -310,6 +321,7 @@ CatchAnnotation.prototype = { this.TEMPLATENAMES = [ "annotationList", //Main "annotationPublicPrivate", //Main->PublicPrivate + "annotationPublicPrivateInstructor", //Main->PublicPrivateInstructor "annotationMediaSelector", //Main->MediaSelector "annotationItem", //Main->ContainerRow "annotationReply",//Main->ContainerRow->Reply @@ -317,8 +329,8 @@ CatchAnnotation.prototype = { "annotationDetail",//Main->ContainerRow->DetailRow ]; //annotator - var wrapper = $('.annotator-wrapper').parent()[0], - annotator = $.data(wrapper, 'annotator'); + var wrapper = $('.annotator-wrapper').parent()[0]; + var annotator = $.data(wrapper, 'annotator'); this.annotator = annotator; //Subscribe to annotator @@ -336,10 +348,22 @@ CatchAnnotation.prototype = { this.HTMLTEMPLATES = CatchSources.HTMLTEMPLATES(this.options.imageUrlRoot); this.TEMPLATES = {}; this._compileTemplates(); + + // the default annotations are the user's personal ones instead of instructor + // if the default tab is instructor, we must refresh the catch to pull the ones + // under the instructor's email. passing empty strings as arguments will default + // to pulling the annotations for the email within this.options.userId. + if(this.options.default_tab.toLowerCase() == 'instructor'){ + this.options.userId = this.options.instructor_email; + this._refresh('',''); + } }, // // GLOBAL UTILITIES // + getTemplate: function(templateName){ + return this.TEMPLATES[templateName]() || ''; + }, refreshCatch: function(newInstance) { var mediaType = this.options.media || 'text', annotationItems = [], @@ -380,10 +404,16 @@ CatchAnnotation.prototype = { if (newInstance){ var videoFormat = (mediaType === "video") ? true:false; + var publicPrivateTemplate = ''; + if (self.options.showPublicPrivate) { + var templateName = this.options.instructor_email ? + "annotationPublicPrivateInstructor" : + "annotationPublicPrivate"; + } el.html(self.TEMPLATES.annotationList({ annotationItems: annotationItems, videoFormat: videoFormat, - PublicPrivate: self.options.showPublicPrivate?self.TEMPLATES.annotationPublicPrivate():'', + PublicPrivate: this.getTemplate(templateName), MediaSelector: self.options.showMediaSelector?self.TEMPLATES.annotationMediaSelector():'', })); }else{ @@ -402,10 +432,21 @@ CatchAnnotation.prototype = { //Set PublicPrivate var PublicPrivateButtons = el.find('.annotationListButtons .PublicPrivate').removeClass('active'); //reset for (var index=0;index= 0)?'&osdaId='+osdaId:'?osdaId='+osdaId; + location.href = uri; + } for(var item in allannotations){ var an = allannotations[item]; if (typeof an.id!='undefined' && an.id == osdaId){//this is the annotation @@ -1011,8 +1065,18 @@ CatchAnnotation.prototype = { userId = ''; //Get userI - userId = (action.html()=="myNotes")? this.annotator.plugins.Permissions.user.id : ''; - + switch (action.html()){ + case 'public': + userId = ''; + break; + case 'instructor': + userId = this.options.instructor_email; + break; + default: + userId = this.annotator.plugins.Permissions.user.id; + break; + } + this.current_tab = action.html(); //Change userid and refresh this.changeUserId(userId); }, @@ -1069,6 +1133,9 @@ CatchAnnotation.prototype = { this._refresh(searchtype,searchInput); }, + _onClearSearchButtonClick: function(evt){ + this._refresh('',''); + }, _clearAnnotator: function(){ var annotator = this.annotator, store = annotator.plugins.Store, diff --git a/common/static/js/vendor/ova/openseadragon.js b/common/static/js/vendor/ova/openseadragon.js index 789c0ce403..559aa5bcd7 100644 --- a/common/static/js/vendor/ova/openseadragon.js +++ b/common/static/js/vendor/ova/openseadragon.js @@ -7872,14 +7872,14 @@ $.extend( $.IIIF1_1TileSource.prototype, $.TileSource.prototype, { uri; if ( level_width < this.tile_width && level_height < this.tile_height ){ - iiif_size = level_width + "," + level_height; + iiif_size = level_width + ","; iiif_region = 'full'; } else { iiif_tile_x = x * iiif_tile_size_width; iiif_tile_y = y * iiif_tile_size_height; iiif_tile_w = Math.min( iiif_tile_size_width, this.width - iiif_tile_x ); iiif_tile_h = Math.min( iiif_tile_size_height, this.height - iiif_tile_y ); - iiif_size = Math.ceil(iiif_tile_w * scale) + "," + Math.ceil(iiif_tile_h * scale); + iiif_size = Math.ceil(iiif_tile_w * scale) + ","; iiif_region = [ iiif_tile_x, iiif_tile_y, iiif_tile_w, iiif_tile_h ].join(','); } uri = [ this['@id'], iiif_region, iiif_size, IIIF_ROTATION, IIIF_QUALITY ].join('/'); diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index 105eab04e4..cbcad9ea2b 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -181,7 +181,7 @@ class XQueueCertInterface(object): course_name = course.display_name or course_id.to_deprecated_string() is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() grade = grades.grade(student, self.request, course) - enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id) + enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id) mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified) user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student) diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py index 59820ceedf..fb2ebe39eb 100644 --- a/lms/djangoapps/notes/views.py +++ b/lms/djangoapps/notes/views.py @@ -11,9 +11,7 @@ from xmodule.modulestore.locations import SlashSeparatedCourseKey @login_required def notes(request, course_id): ''' Displays the student's notes. ''' - course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) - course = get_course_with_access(request.user, 'load', course_key) if not notes_enabled_for_course(course): raise Http404 @@ -28,6 +26,7 @@ def notes(request, course_id): 'student': student, 'storage': storage, 'token': retrieve_token(student.email, course.annotation_token_secret), + 'default_tab': 'myNotes', } return render_to_response('notes.html', context) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 17d1513667..0d2754ff7c 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -65,7 +65,7 @@ class VerifyView(View): reverse('verify_student_verified', kwargs={'course_id': course_id.to_deprecated_string()}) + "?upgrade={}".format(upgrade) ) - elif CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': + elif CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True): return redirect(reverse('dashboard')) else: # If they haven't completed a verification attempt, we have to @@ -119,7 +119,7 @@ class VerifiedView(View): """ upgrade = request.GET.get('upgrade', False) course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) - if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': + if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True): return redirect(reverse('dashboard')) verify_mode = CourseMode.mode_for_course(course_id, "verified") @@ -284,7 +284,7 @@ def show_requirements(request, course_id): Show the requirements necessary for the verification flow. """ course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) - if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': + if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True): return redirect(reverse('dashboard')) upgrade = request.GET.get('upgrade', False) diff --git a/lms/templates/imageannotation.html b/lms/templates/imageannotation.html index 3342d296bb..74ce462560 100644 --- a/lms/templates/imageannotation.html +++ b/lms/templates/imageannotation.html @@ -9,72 +9,69 @@
-
- % if display_name is not UNDEFINED and display_name is not None: -
${display_name}
- % endif -
- % if instructions_html is not UNDEFINED and instructions_html is not None: -
-
- ${_('Instructions')} - ${_('Collapse Instructions')} -
-
- ${instructions_html} -
-
- % endif -
-
-
+
+ % if display_name is not UNDEFINED and display_name is not None: +
${display_name}
+ % endif +
+ % if instructions_html is not UNDEFINED and instructions_html is not None: +
+
+ ${_('Instructions')} + ${_('Collapse Instructions')} +
+
+ ${instructions_html} +
+
+ % endif +
+
+
<%namespace name='static' file='/static_content.html'/> ${static.css(group='style-vendor-tinymce-content', raw=True)} ${static.css(group='style-vendor-tinymce-skin', raw=True)} - \ No newline at end of file diff --git a/lms/templates/notes.html b/lms/templates/notes.html index 58f81be274..569393daaa 100644 --- a/lms/templates/notes.html +++ b/lms/templates/notes.html @@ -1,6 +1,10 @@ <%! from django.utils.translation import ugettext as _ %> <%namespace name='static' file='static_content.html'/> +${static.css(group='style-vendor-tinymce-content', raw=True)} +${static.css(group='style-vendor-tinymce-skin', raw=True)} + + <%inherit file="main.html" /> <%! from django.core.urlresolvers import reverse @@ -102,7 +106,7 @@ if (annotation.permissions) { tokens = annotation.permissions[action] || []; if (is_staff){ - return true; + return true; } if (tokens.length === 0) { return true; @@ -128,7 +132,7 @@ }, }, auth: { - tokenUrl: location.protocol+'//'+location.host+"/token?course_id=${course.id.to_deprecated_string()}" + token: "${token}" }, store: { // The endpoint of the store on your server. @@ -158,37 +162,34 @@ optionsRichText: { tinymce:{ selector: "li.annotator-item textarea", - plugins: "media image insertdatetime link code", + plugins: "media image codemirror", menubar: false, toolbar_items_size: 'small', extended_valid_elements : "iframe[src|frameborder|style|scrolling|class|width|height|name|align|id]", - toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media rubric | code ", + toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image rubric | code ", } - return true; - }, - }, - auth: { - token: "${token}" - }, - store: { - // The endpoint of the store on your server. - prefix: "${storage}", - - annotationData: {}, - - urls: { - // These are the default URLs. - create: '/create', - read: '/read/:id', - update: '/update/:id', - destroy: '/delete/:id', - search: '/search' }, + auth: { + token: "${token}" + }, + store: { + // The endpoint of the store on your server. + prefix: "${storage}", + + annotationData: {}, + + urls: { + // These are the default URLs. + create: '/create', + read: '/read/:id', + update: '/update/:id', + destroy: '/delete/:id', + search: '/search' + }, + } }; - tinyMCE.baseURL = "${settings.STATIC_URL}" + "js/vendor/ova"; var imgURLRoot = "${settings.STATIC_URL}" + "js/vendor/ova/catch/img/"; - //remove old instances if (Annotator._instances.length !== 0) { $('#notesHolder').annotator("destroy"); @@ -207,7 +208,8 @@ showMediaSelector: true, showPublicPrivate: true, pagination:pagination,//Number of Annotations per load in the pagination, - flags:is_staff + flags:is_staff, + default_tab: "${default_tab}", }, Catch = new CatchAnnotation($('#catchDIV'),catchOptions); diff --git a/lms/templates/textannotation.html b/lms/templates/textannotation.html index dc6ba4bcfb..cb0cd23a35 100644 --- a/lms/templates/textannotation.html +++ b/lms/templates/textannotation.html @@ -2,8 +2,6 @@ <%namespace name='static' file='/static_content.html'/> ${static.css(group='style-vendor-tinymce-content', raw=True)} ${static.css(group='style-vendor-tinymce-skin', raw=True)} - diff --git a/lms/templates/videoannotation.html b/lms/templates/videoannotation.html index dc917c158b..07b61213c5 100644 --- a/lms/templates/videoannotation.html +++ b/lms/templates/videoannotation.html @@ -2,9 +2,6 @@ <%namespace name='static' file='/static_content.html'/> ${static.css(group='style-vendor-tinymce-content', raw=True)} ${static.css(group='style-vendor-tinymce-skin', raw=True)} -