From b1653f056143216ec0026c2f6efcc30df2e2a680 Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Thu, 11 Jul 2013 11:19:39 -0400 Subject: [PATCH 01/57] layout cleanup on LMS PDF Textbook viewer --- common/static/css/pdfviewer.css | 3 +- lms/static/sass/course/_textbook.scss | 59 +++++++++++++++++++---- lms/templates/static_pdfbook.html | 67 ++++++++++++++------------- 3 files changed, 87 insertions(+), 42 deletions(-) diff --git a/common/static/css/pdfviewer.css b/common/static/css/pdfviewer.css index 656bc47c29..8b0253261b 100644 --- a/common/static/css/pdfviewer.css +++ b/common/static/css/pdfviewer.css @@ -100,7 +100,7 @@ select { .toolbar { /* position: absolute; */ left: 0; - right: 0; + right: 0; height: 32px; z-index: 9999; cursor: default; @@ -185,6 +185,7 @@ select { margin: 0; } +.splitToolbarButton > .toolbarButton, /*added */ .splitToolbarButton:hover > .toolbarButton, .splitToolbarButton:focus > .toolbarButton, .splitToolbarButton.toggled > .toolbarButton, diff --git a/lms/static/sass/course/_textbook.scss b/lms/static/sass/course/_textbook.scss index bc9da1f43f..08f1a853dc 100644 --- a/lms/static/sass/course/_textbook.scss +++ b/lms/static/sass/course/_textbook.scss @@ -1,13 +1,46 @@ div.book-wrapper { - display: table; - table-layout: fixed; - padding: 1em 8em; + max-width: 1150px; + margin: 0 auto; + width: 100%; + background-color: $white; + #toolbarViewer { + padding: 0 ($baseline/2); + + #toolbarViewerLeft { + display: inline-block; + } + + .outerCenter { + display: inline-block; + float: right !important; + right: auto; + + .innerCenter { + right: auto; + } + + .dropdownToolbarButton { + margin: 3px 2px 4px 0; + } + } + + + } #open_close_accordion { display: none; } + .pdfbook-wrap { + display: table; + width: 100%; + } + + .pdfbook-wrap-inner { + display: table-row; + } + section.book-sidebar { @extend .sidebar; @extend .tran; @@ -44,14 +77,17 @@ div.book-wrapper { li { background: none; border-bottom: 0; - padding-left: lh(); + padding-left: ($baseline/2); a { - padding: 0; @include clearfix; + padding: 0; + color: $link-color; + cursor: pointer; &:hover { background-color: transparent; + color: $link-hover; .page-number { opacity: 1.0; @@ -84,7 +120,7 @@ div.book-wrapper { > li { padding: 5px 6px; - margin: 0 16px 5px 25px; + margin: ($baseline/4) ($baseline/2); } } } @@ -158,18 +194,20 @@ div.book-wrapper { } section.page { - border: 1px solid $border-color; + border-left: 1px solid $border-color; background-color: #fff; position: relative; text-align: center; - padding: lh(); - margin-right:10%; border-radius: 0 3px 3px 0; img { max-width: 100%; } + #viewer { + padding: $baseline; + } + div { text-align: left; line-height: 1.6em; @@ -214,3 +252,6 @@ div.book-wrapper { } } } + + + diff --git a/lms/templates/static_pdfbook.html b/lms/templates/static_pdfbook.html index 565a59977a..a8608b2877 100644 --- a/lms/templates/static_pdfbook.html +++ b/lms/templates/static_pdfbook.html @@ -11,7 +11,7 @@ <%static:js group='courseware'/> - + <%block name="js_extra"> @@ -35,10 +35,10 @@ %if page is not None: options.pageNum = ${page}; %endif - + $('#outerContainer').PDFViewer(options); }); - + <%include file="/courseware/course_navigation.html" args="active_page='pdftextbook/{0}'.format(book_index)" /> @@ -91,40 +91,43 @@ %if 'chapters' in textbook: -
- +
%endif -
+
- -
-
-
- + +
+
+ + + From 08fe23ac5f0c872b1b5784cb81cdc6f2e1c98a61 Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Thu, 11 Jul 2013 11:26:59 -0400 Subject: [PATCH 02/57] adjusted the LMS PDF textbook zoom elements positioning --- lms/static/sass/course/_textbook.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/static/sass/course/_textbook.scss b/lms/static/sass/course/_textbook.scss index 08f1a853dc..72d73bdb78 100644 --- a/lms/static/sass/course/_textbook.scss +++ b/lms/static/sass/course/_textbook.scss @@ -13,8 +13,8 @@ div.book-wrapper { .outerCenter { display: inline-block; - float: right !important; - right: auto; + position: absolute; + right: ($baseline/2); .innerCenter { right: auto; From 84f531af54f91f7aa1ef7e28afc698db778c49c4 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 12 Jul 2013 13:40:55 +0000 Subject: [PATCH 03/57] add export_all_courses management script to cms --- .../management/commands/export.py | 2 +- .../management/commands/export_all_courses.py | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 cms/djangoapps/contentstore/management/commands/export_all_courses.py diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py index eb7800d46c..0087fdddcb 100644 --- a/cms/djangoapps/contentstore/management/commands/export.py +++ b/cms/djangoapps/contentstore/management/commands/export.py @@ -14,7 +14,7 @@ unnamed_modules = 0 class Command(BaseCommand): - help = 'Import the specified data directory into the default ModuleStore' + help = 'Export the specified data directory into the default ModuleStore' def handle(self, *args, **options): if len(args) != 2: diff --git a/cms/djangoapps/contentstore/management/commands/export_all_courses.py b/cms/djangoapps/contentstore/management/commands/export_all_courses.py new file mode 100644 index 0000000000..978418ba7b --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/export_all_courses.py @@ -0,0 +1,49 @@ +### +### Script for exporting all courseware from Mongo to a directory +### +import os + +from django.core.management.base import BaseCommand, CommandError +from xmodule.modulestore.xml_exporter import export_to_xml +from xmodule.modulestore.django import modulestore +from xmodule.contentstore.django import contentstore +from xmodule.course_module import CourseDescriptor + + +unnamed_modules = 0 + + +class Command(BaseCommand): + help = 'Export all courses from mongo to the specified data directory' + + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("import requires one argument: ") + + output_path = args[0] + + cs = contentstore() + ms = modulestore('direct') + root_dir = output_path + courses = ms.get_courses() + + print "%d courses to export:" % len(courses) + cids = [x.id for x in courses] + print cids + + for course_id in cids: + + print "-"*77 + print "Exporting course id = {0} to {1}".format(course_id, output_path) + + if 1: + try: + location = CourseDescriptor.id_to_location(course_id) + course_dir = course_id.replace('/','...') + export_to_xml(ms, cs, location, root_dir, course_dir) + except Exception as err: + print "="*30 + "> Oops, failed to export %s" % course_id + print "Error:" + print err + + From 84c38b4e9a58e6ea2c317212a8fe96dc58195c32 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 12 Jul 2013 09:43:52 -0400 Subject: [PATCH 04/57] fix two typos in export scripts --- cms/djangoapps/contentstore/management/commands/export.py | 2 +- .../contentstore/management/commands/export_all_courses.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py index 0087fdddcb..52a5eb265b 100644 --- a/cms/djangoapps/contentstore/management/commands/export.py +++ b/cms/djangoapps/contentstore/management/commands/export.py @@ -18,7 +18,7 @@ class Command(BaseCommand): def handle(self, *args, **options): if len(args) != 2: - raise CommandError("import requires two arguments: ") + raise CommandError("export requires two arguments: ") course_id = args[0] output_path = args[1] diff --git a/cms/djangoapps/contentstore/management/commands/export_all_courses.py b/cms/djangoapps/contentstore/management/commands/export_all_courses.py index 978418ba7b..5d0adaa315 100644 --- a/cms/djangoapps/contentstore/management/commands/export_all_courses.py +++ b/cms/djangoapps/contentstore/management/commands/export_all_courses.py @@ -18,7 +18,7 @@ class Command(BaseCommand): def handle(self, *args, **options): if len(args) != 1: - raise CommandError("import requires one argument: ") + raise CommandError("export requires one argument: ") output_path = args[0] From 08b8438dda9a5e6ec2e8b4b3136a189160bf561d Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 12 Jul 2013 14:10:16 +0000 Subject: [PATCH 05/57] add extra modulestore() argument to export to make it export drafts also --- .../contentstore/management/commands/export_all_courses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/management/commands/export_all_courses.py b/cms/djangoapps/contentstore/management/commands/export_all_courses.py index 5d0adaa315..3c7c398e94 100644 --- a/cms/djangoapps/contentstore/management/commands/export_all_courses.py +++ b/cms/djangoapps/contentstore/management/commands/export_all_courses.py @@ -40,7 +40,7 @@ class Command(BaseCommand): try: location = CourseDescriptor.id_to_location(course_id) course_dir = course_id.replace('/','...') - export_to_xml(ms, cs, location, root_dir, course_dir) + export_to_xml(ms, cs, location, root_dir, course_dir, modulestore()) except Exception as err: print "="*30 + "> Oops, failed to export %s" % course_id print "Error:" From c53dd97f79827a3cfd818cf90f88f6bc94319cd4 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 12 Jul 2013 14:11:20 +0000 Subject: [PATCH 06/57] add extra modulestore() argument to single export script --- cms/djangoapps/contentstore/management/commands/export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py index 52a5eb265b..90db8750d9 100644 --- a/cms/djangoapps/contentstore/management/commands/export.py +++ b/cms/djangoapps/contentstore/management/commands/export.py @@ -30,4 +30,4 @@ class Command(BaseCommand): root_dir = os.path.dirname(output_path) course_dir = os.path.splitext(os.path.basename(output_path))[0] - export_to_xml(modulestore('direct'), contentstore(), location, root_dir, course_dir) + export_to_xml(modulestore('direct'), contentstore(), location, root_dir, course_dir, modulestore()) From f27ca66f51e555779f1ebba6f5167671d55dd192 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 12 Jul 2013 10:11:39 -0400 Subject: [PATCH 07/57] Added documentation to new acceptance test features and techniques --- doc/testing.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/doc/testing.md b/doc/testing.md index b40ba30610..96f0dcfd98 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -175,6 +175,8 @@ Use `rake -T` to get a list of all available subsystems **Troubleshooting**: If you get an error message while running the `rake` task, try running `bundle install` to install the required ruby gems. +Unit tests can be run in parallel to each other and while acceptance tests are running + ### Running Acceptance Tests We use [Lettuce](http://lettuce.it/) for acceptance testing. @@ -203,6 +205,10 @@ To start the debugger on failure, add the `--pdb` option: To run tests faster by not collecting static files, you can use `rake fasttest_acceptance_lms` and `rake fasttest_acceptance_cms`. +Acceptance tests will run on a randomized port and can be run in the background of rake cms and lms or unit tests. +To specify the port, change the LETTUCE_SERVER_PORT constant in cms/envs/acceptance.py and lms/envs/acceptance.py +as well as the port listed in cms/djangoapps/contentstore/feature/upload.py + **Note**: The acceptance tests can *not* currently run in parallel. ## Viewing Test Coverage @@ -230,3 +236,30 @@ When testing problems that use a queue server on AWS (e.g. sandbox-xqueue.edx.or `django-admin.py runserver --settings=lms.envs.dev --pythonpath=. 0.0.0.0:8000` When you connect to the LMS, you need to use the public ip. Use `ifconfig` to figure out the number, and connect e.g. to `http://18.3.4.5:8000/` + + +## Acceptance Test Techniques + +1. Do not assert not if possible for css. Use world.is_css_present and is_css_not_present + Errors can arise if checks for the css are performed before the page finishes loading. + To get around this, there are functions that will wait a period of time for the css to appear + before returning and return immediately if they are there. There is a reverse to this function as well. + It will wait for the css to not appear and returns if it isn't there. + + All css functions can utilize this timeout to ensure that the page is fully loaded + +2. Dealing with alerts + Chrome can hang on javascripts alerts. If a javascript alert/prompt/confirmation is expected, use the step + 'I will confirm all alerts', 'I will cancel all alerts' or 'I will anser all prompts with "(.*)"' before the step + that causes the alert in order to properly deal with it. + +3. Dealing with stale element reference exceptions + These exceptions happen if any part of the page is refreshed in between finding an element and accessing the element. + When possible, use any of the css functions in common/djangoapps/terrain/ui_helpers.py as they will retry the action + in case of this exception. If the functionality is not there, wrap the function with world.retry_on_exception. This function takes in a function and will retry and return the result of the function if there was an exception + +4. Scenario Level Constants + If you want an object to be available for the entire scenario, it can be stored in world.scenario_dict. This object + is a dictionary that gets refreshed at the beginning on the scenario. Currently, the current logged in user and the current created course are stored under 'COURSE' and 'USER'. This will help prevent strings from being hard coded so the + acceptance tests can become more flexible. + From bdca0f1d0e5be6fe15a13d48e4b7e2bd9f55eb1c Mon Sep 17 00:00:00 2001 From: Xavier Antoviaque Date: Fri, 12 Jul 2013 11:15:00 -0300 Subject: [PATCH 08/57] vagrant: Always mount `node_modules` folder after Vagrant NFS mount --- scripts/vagrant-provisioning.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/vagrant-provisioning.sh b/scripts/vagrant-provisioning.sh index 153bc7a879..d4f627b2c4 100755 --- a/scripts/vagrant-provisioning.sh +++ b/scripts/vagrant-provisioning.sh @@ -59,7 +59,14 @@ chown vagrant.vagrant ~vagrant/.ssh/known_hosts # Node modules require a filesystem with symlinks (Windows support) mkdir -p /opt/edx/node_modules /opt/edx/edx-platform/node_modules -mount -o bind /opt/edx/node_modules /opt/edx/edx-platform/node_modules +([[ -f /etc/fstab ]] && grep '/opt/edx/node_modules' /etc/fstab) || { + echo '/opt/edx/node_modules /opt/edx/edx-platform/node_modules none bind,noauto 0 0' >> /etc/fstab + mount /opt/edx/node_modules +} +# Must be mounted *after* the NFS mount, made manually by Vagrant +([[ -f /etc/cron.d/nodemodules ]] && grep '/opt/edx/node_modules' /etc/cron.d/nodemodules) || { + echo '@reboot root until [ -n "`mount |grep "/opt/edx/edx-platform type"`" ]; do sleep 1; done; mount /opt/edx/node_modules' > /etc/cron.d/nodemodules +} # Force rechecking all prerequisites (could have been fetched outside of the VM) rm -rf /opt/edx/edx-platform/.prereqs_cache From e62cb45da4242578f4369afdf40b4f47c1474336 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 8 Jul 2013 15:50:22 +0000 Subject: [PATCH 09/57] tracking in idashboard should only log json-serializable objects --- lms/djangoapps/instructor/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 9f9b7a2399..b046bdb762 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -374,7 +374,7 @@ def instructor_dashboard(request, course_id): msg += message if student is not None: progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student.id}) - track.views.server_track(request, "get-student-progress-page", {"student": student, "instructor": request.user, "course": course_id}, page="idashboard") + track.views.server_track(request, "get-student-progress-page", {"student": str(student), "instructor": str(request.user), "course": course_id}, page="idashboard") msg += " Progress page for username: {1} with email address: {2}.".format(progress_url, student.username, student.email) #---------------------------------------- @@ -472,7 +472,7 @@ def instructor_dashboard(request, course_id): msg += 'Added {0} to instructor group = {1}'.format(user, group.name) log.debug('staffgrp={0}'.format(group.name)) user.groups.add(group) - track.views.server_track(request, "add-instructor", {"instructor": user}, page="idashboard") + track.views.server_track(request, "add-instructor", {"instructor": str(user)}, page="idashboard") elif action == 'Remove course staff': uname = request.POST['staffuser'] @@ -491,7 +491,7 @@ def instructor_dashboard(request, course_id): msg += 'Removed {0} from instructor group = {1}'.format(user, group.name) log.debug('instructorgrp={0}'.format(group.name)) user.groups.remove(group) - track.views.server_track(request, "remove-instructor", {"instructor": user}, page="idashboard") + track.views.server_track(request, "remove-instructor", {"instructor": str(user)}, page="idashboard") #---------------------------------------- # DataDump @@ -658,7 +658,7 @@ def instructor_dashboard(request, course_id): problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg - track.views.server_track(request, "psychometrics-histogram-generation", {"problem": problem}, page="idashboard") + track.views.server_track(request, "psychometrics-histogram-generation", {"problem": str(problem)}, page="idashboard") if idash_mode == 'Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_id) @@ -911,7 +911,7 @@ def _add_or_remove_user_group(request, username_or_email, group, group_title, ev else: user.groups.remove(group) event = "add" if do_add else "remove" - track.views.server_track(request, "add-or-remove-user-group", {"event_name": event_name, "user": user, "event": event}, page="idashboard") + track.views.server_track(request, "add-or-remove-user-group", {"event_name": event_name, "user": str(user), "event": event}, page="idashboard") return msg From a3e4f21a9c891735765f96e61fd302e8bb774c17 Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 9 Jul 2013 14:24:23 -0400 Subject: [PATCH 10/57] use unicode instead of str for casting track.views args in idashboard --- lms/djangoapps/instructor/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index b046bdb762..d133bdaf38 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -374,7 +374,7 @@ def instructor_dashboard(request, course_id): msg += message if student is not None: progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student.id}) - track.views.server_track(request, "get-student-progress-page", {"student": str(student), "instructor": str(request.user), "course": course_id}, page="idashboard") + track.views.server_track(request, "get-student-progress-page", {"student": unicode(student), "instructor": unicode(request.user), "course": course_id}, page="idashboard") msg += " Progress page for username: {1} with email address: {2}.".format(progress_url, student.username, student.email) #---------------------------------------- @@ -472,7 +472,7 @@ def instructor_dashboard(request, course_id): msg += 'Added {0} to instructor group = {1}'.format(user, group.name) log.debug('staffgrp={0}'.format(group.name)) user.groups.add(group) - track.views.server_track(request, "add-instructor", {"instructor": str(user)}, page="idashboard") + track.views.server_track(request, "add-instructor", {"instructor": unicode(user)}, page="idashboard") elif action == 'Remove course staff': uname = request.POST['staffuser'] @@ -491,7 +491,7 @@ def instructor_dashboard(request, course_id): msg += 'Removed {0} from instructor group = {1}'.format(user, group.name) log.debug('instructorgrp={0}'.format(group.name)) user.groups.remove(group) - track.views.server_track(request, "remove-instructor", {"instructor": str(user)}, page="idashboard") + track.views.server_track(request, "remove-instructor", {"instructor": unicode(user)}, page="idashboard") #---------------------------------------- # DataDump @@ -658,7 +658,7 @@ def instructor_dashboard(request, course_id): problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg - track.views.server_track(request, "psychometrics-histogram-generation", {"problem": str(problem)}, page="idashboard") + track.views.server_track(request, "psychometrics-histogram-generation", {"problem": unicode(problem)}, page="idashboard") if idash_mode == 'Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_id) @@ -911,7 +911,7 @@ def _add_or_remove_user_group(request, username_or_email, group, group_title, ev else: user.groups.remove(group) event = "add" if do_add else "remove" - track.views.server_track(request, "add-or-remove-user-group", {"event_name": event_name, "user": str(user), "event": event}, page="idashboard") + track.views.server_track(request, "add-or-remove-user-group", {"event_name": event_name, "user": unicode(user), "event": event}, page="idashboard") return msg From 50bea28d750a8d5bfbe9ff31b5bdef18c2957bdd Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 12 Jul 2013 11:13:59 -0400 Subject: [PATCH 11/57] Add a 'string literal' serialization method and use it in the lambda function mapping table when the value to xml serialize is of basestring type. Also a couple of drive-by pep8 fixes. --- common/lib/xmodule/xmodule/xml_module.py | 54 ++++++++++++++---------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index c1340a9fc0..edd10288de 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -81,13 +81,23 @@ class AttrMap(_AttrMapBase): def serialize_field(value): """ - Return a string version of the value (where value is the JSON-formatted, internally stored value). + Return a string version of the value (where value is the JSON-formatted, internally stored value). - By default, this is the result of calling json.dumps on the input value. - """ + By default, this is the result of calling json.dumps on the input value. + """ return json.dumps(value, cls=EdxJSONEncoder) +def serialize_string_literal(value): + """ + Assert that the value is a base string and - if it is - simply return it + """ + if not isinstance(value, basestring): + raise Exception('Value {0} is not of type basestring!'.format(value)) + + return value + + def deserialize_field(field, value): """ Deserialize the string version to the value stored internally. @@ -126,7 +136,7 @@ class XmlDescriptor(XModuleDescriptor): """ xml_attributes = Dict(help="Map of unhandled xml attributes, used only for storage between import and export", - default={}, scope=Scope.settings) + default={}, scope=Scope.settings) # Extension to append to filename paths filename_extension = 'xml' @@ -141,23 +151,23 @@ class XmlDescriptor(XModuleDescriptor): # understand? And if we do, is this the place? # Related: What's the right behavior for clean_metadata? metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize', - 'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc', - 'ispublic', # if True, then course is listed for all users; see - 'xqa_key', # for xqaa server access - 'giturl', # url of git server for origin of file - # information about testcenter exams is a dict (of dicts), not a string, - # so it cannot be easily exportable as a course element's attribute. - 'testcenter_info', - # VS[compat] Remove once unused. - 'name', 'slug') + 'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc', + 'ispublic', # if True, then course is listed for all users; see + 'xqa_key', # for xqaa server access + 'giturl', # url of git server for origin of file + # information about testcenter exams is a dict (of dicts), not a string, + # so it cannot be easily exportable as a course element's attribute. + 'testcenter_info', + # VS[compat] Remove once unused. + 'name', 'slug') metadata_to_strip = ('data_dir', - 'tabs', 'grading_policy', 'published_by', 'published_date', - 'discussion_blackouts', 'testcenter_info', - # VS[compat] -- remove the below attrs once everything is in the CMS - 'course', 'org', 'url_name', 'filename', - # Used for storing xml attributes between import and export, for roundtrips - 'xml_attributes') + 'tabs', 'grading_policy', 'published_by', 'published_date', + 'discussion_blackouts', 'testcenter_info', + # VS[compat] -- remove the below attrs once everything is in the CMS + 'course', 'org', 'url_name', 'filename', + # Used for storing xml attributes between import and export, for roundtrips + 'xml_attributes') metadata_to_export_to_policy = ('discussion_topics') @@ -166,7 +176,7 @@ class XmlDescriptor(XModuleDescriptor): for field in set(cls.fields + cls.lms.fields): if field.name == attr: from_xml = lambda val: deserialize_field(field, val) - to_xml = lambda val : serialize_field(val) + to_xml = lambda val: serialize_string_literal(val) if isinstance(val, basestring) else serialize_field(val) return AttrMap(from_xml, to_xml) return AttrMap() @@ -254,7 +264,7 @@ class XmlDescriptor(XModuleDescriptor): definition, children = cls.definition_from_xml(definition_xml, system) if definition_metadata: definition['definition_metadata'] = definition_metadata - definition['filename'] = [ filepath, filename ] + definition['filename'] = [filepath, filename] return definition, children @@ -280,7 +290,6 @@ class XmlDescriptor(XModuleDescriptor): metadata[attr] = attr_map.from_xml(val) return metadata - @classmethod def apply_policy(cls, metadata, policy): """ @@ -374,7 +383,6 @@ class XmlDescriptor(XModuleDescriptor): """ return True - def export_to_xml(self, resource_fs): """ Returns an xml string representing this module, and all modules From 5aae3c69715bb84294354299f26fbcf07388c0d6 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 12 Jul 2013 11:14:35 -0400 Subject: [PATCH 12/57] add new tests for the string literal serialization --- .../xmodule/xmodule/tests/test_xml_module.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py index 6581ce58f6..a853e6ab3f 100644 --- a/common/lib/xmodule/xmodule/tests/test_xml_module.py +++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py @@ -4,7 +4,7 @@ from xmodule.x_module import XModuleFields from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List from xmodule.fields import Date, Timedelta -from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field +from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field, serialize_string_literal import unittest from .import get_test_system from nose.tools import assert_equals @@ -186,12 +186,27 @@ class TestSerialize(unittest.TestCase): assert_equals('"false"', serialize_field('false')) assert_equals('"fAlse"', serialize_field('fAlse')) assert_equals('"hat box"', serialize_field('hat box')) - assert_equals('{"bar": "hat", "frog": "green"}', serialize_field({'bar': 'hat', 'frog' : 'green'})) + assert_equals('{"bar": "hat", "frog": "green"}', serialize_field({'bar': 'hat', 'frog': 'green'})) assert_equals('[3.5, 5.6]', serialize_field([3.5, 5.6])) assert_equals('["foo", "bar"]', serialize_field(['foo', 'bar'])) assert_equals('"2012-12-31T23:59:59Z"', serialize_field("2012-12-31T23:59:59Z")) assert_equals('"1 day 12 hours 59 minutes 59 seconds"', - serialize_field("1 day 12 hours 59 minutes 59 seconds")) + serialize_field("1 day 12 hours 59 minutes 59 seconds")) + + def test_serialize_string_literal(self): + assert_equals('2', serialize_string_literal('2')) + assert_equals('2.589', serialize_string_literal('2.589')) + assert_equals('false', serialize_string_literal('false')) + assert_equals('fAlse', serialize_string_literal('fAlse')) + assert_equals('hat box', serialize_string_literal('hat box')) + assert_equals('2012-12-31T23:59:59Z', serialize_string_literal("2012-12-31T23:59:59Z")) + assert_equals('1 day 12 hours 59 minutes 59 seconds', + serialize_string_literal("1 day 12 hours 59 minutes 59 seconds")) + + try: + self.assertRaises(serialize_string_literal(2.31)) + except Exception: + pass class TestDeserialize(unittest.TestCase): From ab4012cc594341ac78663bd89f90621cb8c02bd3 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 12 Jul 2013 11:33:46 -0400 Subject: [PATCH 13/57] fix up some of the test courses which had hardcoded that quot; escaping --- common/test/data/simple/course.xml | 8 ++++---- .../test/data/test_exam_registration/course/2012_Fall.xml | 4 ++-- common/test/data/test_start_date/course/2012_Fall.xml | 4 ++-- common/test/data/toy/chapter/secret/magic.xml | 2 +- common/test/data/toy/course/2012_Fall.xml | 8 ++++---- common/test/data/two_toys/video/Video_Resources.xml | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/common/test/data/simple/course.xml b/common/test/data/simple/course.xml index c9bb5ec8b2..529528ca0a 100644 --- a/common/test/data/simple/course.xml +++ b/common/test/data/simple/course.xml @@ -1,13 +1,13 @@ -