Merge pull request #10981 from edx/rc/2015-12-16

Rc/2015 12 16
This commit is contained in:
Diana Huang
2015-12-16 10:35:54 -05:00
732 changed files with 10444 additions and 1659652 deletions

View File

@@ -259,3 +259,4 @@ Afeef Janjua <janjua.afeef@gmail.com>
Jacek Bzdak <jbzdak@gmail.com>
Jillian Vogel <pomegranited@gmail.com>
Dan Powell <dan@abakas.com>
Mariana Araújo <simbelm.ne@gmail.com>

View File

@@ -18,6 +18,9 @@ edX product team. You may get some valuable feedback that changes how you think
about your idea, or you may find other developers who have the same idea and want
to work together.
JIRA
----
If you've got an idea for a new feature or new functionality for an existing feature,
please start a discussion on the `edx-code`_ mailing list to get feedback from
the community about the idea and your implementation choices.
@@ -38,14 +41,27 @@ pull request.
.. _start a discussion on JIRA: https://openedx.atlassian.net/secure/Dashboard.jspa
.. _create a free JIRA account: https://openedx.atlassian.net/admin/users/sign-up
For real-time conversation, we use `IRC`_: we all hang out in the
`#edx-code channel on Freenode`_. Come join us! The channel tends to be most
active Monday through Friday between 13:00 and 21:00 UTC
(9am to 5pm US Eastern time), but interesting conversations can happen
at any time.
Slack
-----
.. _IRC: http://www.irchelp.org/
.. _#edx-code channel on Freenode: http://webchat.freenode.net/?channels=edx-code
To talk with others in the Open edX community, join us on `Slack`_.
`Sign up for a free account`_ and join the conversation!
The group tends to be most active Monday through Friday
between 13:00 and 21:00 UTC (9am to 5pm US Eastern time),
but interesting conversations can happen at any time.
There are many different channels available for different topics, including:
* ``#ops`` for installation help
* ``#events`` for upcoming events related to Open edX
* ``#content`` for discussions about course content and creating the best courses
And lots more! You can also make your own channels to discuss new topics.
.. _Slack: https://slack.com/
.. _Sign up for a free account: https://openedx-slack-invite.herokuapp.com/
Mailing Lists
-------------
For asynchronous conversation, we have several mailing lists on Google Groups:

View File

@@ -10,10 +10,10 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy, ugettext as _
from django.core.urlresolvers import resolve
from contentstore.utils import course_image_url
from contentstore.course_group_config import GroupConfiguration
from course_modes.models import CourseMode
from eventtracking import tracker
from openedx.core.lib.courses import course_image_url
from search.search_engine_base import SearchEngine
from xmodule.annotator_mixin import html_to_text
from xmodule.modulestore import ModuleStoreEnum

View File

@@ -1,33 +0,0 @@
@shard_1
Feature: CMS.Course checklists
Scenario: A course author sees checklists defined by edX
Given I have opened a new course in Studio
When I select Checklists from the Tools menu
Then I see the four default edX checklists
Scenario: A course author can mark tasks as complete
Given I have opened Checklists
Then I can check and uncheck tasks in a checklist
And I reload the page
Then the tasks are correctly selected
# There are issues getting link to be active in browsers other than chrome
@skip_firefox
@skip_internetexplorer
@skip_safari
Scenario: A task can link to a location within Studio
Given I have opened Checklists
When I select a link to the course outline
Then I am brought to the course outline page
And I press the browser back button
Then I am brought back to the course outline in the correct state
# There are issues getting link to be active in browsers other than chrome
@skip_firefox
@skip_internetexplorer
@skip_safari
Scenario: A task can link to a location outside Studio
Given I have opened Checklists
When I select a link to help page
Then I am brought to the help page in a new window

View File

@@ -1,125 +0,0 @@
# pylint: disable=missing-docstring
# pylint: disable=redefined-outer-name
from lettuce import world, step
from nose.tools import assert_true, assert_equal
from selenium.common.exceptions import StaleElementReferenceException
############### ACTIONS ####################
@step('I select Checklists from the Tools menu$')
def i_select_checklists(step):
world.click_tools()
link_css = 'li.nav-course-tools-checklists a'
world.css_click(link_css)
world.wait_for_ajax_complete()
@step('I have opened Checklists$')
def i_have_opened_checklists(step):
step.given('I have opened a new course in Studio')
step.given('I select Checklists from the Tools menu')
@step('I see the four default edX checklists$')
def i_see_default_checklists(step):
checklists = world.css_find('.checklist-title')
assert_equal(4, len(checklists))
assert_true(checklists[0].text.endswith('Getting Started With Studio'))
assert_true(checklists[1].text.endswith('Draft a Rough Course Outline'))
assert_true(checklists[2].text.endswith("Explore edX\'s Support Tools"))
assert_true(checklists[3].text.endswith('Draft Your Course About Page'))
@step('I can check and uncheck tasks in a checklist$')
def i_can_check_and_uncheck_tasks(step):
# Use the 2nd checklist as a reference
verifyChecklist2Status(0, 7, 0)
toggleTask(1, 0)
verifyChecklist2Status(1, 7, 14)
toggleTask(1, 3)
verifyChecklist2Status(2, 7, 29)
toggleTask(1, 6)
verifyChecklist2Status(3, 7, 43)
toggleTask(1, 3)
verifyChecklist2Status(2, 7, 29)
@step('the tasks are correctly selected$')
def tasks_correctly_selected(step):
verifyChecklist2Status(2, 7, 29)
# verify that task 7 is still selected by toggling its checkbox state and making sure that it deselects
world.browser.execute_script("window.scrollBy(0,1000)")
toggleTask(1, 6)
verifyChecklist2Status(1, 7, 14)
@step('I select a link to the course outline$')
def i_select_a_link_to_the_course_outline(step):
clickActionLink(1, 0, 'Edit Course Outline')
@step('I am brought to the course outline page$')
def i_am_brought_to_course_outline(step):
assert world.is_css_present('body.view-outline')
assert_equal(1, len(world.browser.windows))
@step('I am brought back to the course outline in the correct state$')
def i_am_brought_back_to_course_outline(step):
step.given('I see the four default edX checklists')
# In a previous step, we selected (1, 0) in order to click the 'Edit Course Outline' link.
# Make sure the task is still showing as selected (there was a caching bug with the collection).
verifyChecklist2Status(1, 7, 14)
@step('I select a link to help page$')
def i_select_a_link_to_the_help_page(step):
clickActionLink(2, 0, 'Visit Studio Help')
@step('I am brought to the help page in a new window$')
def i_am_brought_to_help_page_in_new_window(step):
step.given('I see the four default edX checklists')
windows = world.browser.windows
assert_equal(2, len(windows))
world.browser.switch_to_window(windows[1])
assert_equal('http://help.edge.edx.org/', world.browser.url)
############### HELPER METHODS ####################
def verifyChecklist2Status(completed, total, percentage):
def verify_count(driver):
try:
statusCount = world.css_find('#course-checklist1 .status-count').first
return statusCount.text == str(completed)
except StaleElementReferenceException:
return False
world.wait_for(verify_count)
assert_equal(str(total), world.css_find('#course-checklist1 .status-amount').first.text)
# Would like to check the CSS width, but not sure how to do that.
assert_equal(str(percentage), world.css_find('#course-checklist1 .viz-checklist-status-value .int').first.text)
def toggleTask(checklist, task):
world.css_click('#course-checklist' + str(checklist) + '-task' + str(task))
world.wait_for_ajax_complete()
# TODO: figure out a way to do this in phantom and firefox
# For now we will mark the scenerios that use this method as skipped
def clickActionLink(checklist, task, actionText):
# text will be empty initially, wait for it to populate
def verify_action_link_text(driver):
actualText = world.css_text('#course-checklist' + str(checklist) + ' a', index=task)
if actualText == actionText:
return True
else:
# toggle checklist item to make sure that the link button is showing
toggleTask(checklist, task)
return False
world.wait_for(verify_action_link_text)
world.css_click('#course-checklist' + str(checklist) + ' a', index=task)
world.wait_for_ajax_complete()

View File

@@ -24,32 +24,19 @@ class Command(BaseCommand):
"""
help = '''Delete a MongoDB backed course'''
def handle(self, *args, **options):
if len(args) == 0:
raise CommandError("Arguments missing: 'org/number/run commit'")
def add_arguments(self, parser):
parser.add_argument('course_key', help="ID of the course to delete.")
if len(args) == 1:
if args[0] == 'commit':
raise CommandError("Delete_course requires a course_key <org/number/run> argument.")
else:
raise CommandError("Delete_course requires a commit argument at the end")
elif len(args) == 2:
try:
course_key = CourseKey.from_string(args[0])
except InvalidKeyError:
try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(args[0])
except InvalidKeyError:
raise CommandError("Invalid course_key: '%s'. Proper syntax: 'org/number/run commit' " % args[0])
if args[1] != 'commit':
raise CommandError("Delete_course requires a commit argument at the end")
elif len(args) > 2:
raise CommandError("Too many arguments! Expected <course_key> <commit>")
def handle(self, *args, **options):
try:
course_key = CourseKey.from_string(options['course_key'])
except InvalidKeyError:
raise CommandError("Invalid course_key: '%s'." % options['course_key'])
if not modulestore().get_course(course_key):
raise CommandError("Course with '%s' key not found." % args[0])
raise CommandError("Course with '%s' key not found." % options['course_key'])
print 'Actually going to delete the %s course from DB....' % args[0]
print 'Going to delete the %s course from DB....' % options['course_key']
if query_yes_no("Deleting course {0}. Confirm?".format(course_key), default="no"):
if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
delete_course_and_groups(course_key, ModuleStoreEnum.UserID.mgmt_command)

View File

@@ -6,72 +6,12 @@ import unittest
import mock
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from django.core.management import CommandError
from contentstore.management.commands.delete_course import Command
from django.core.management import call_command, CommandError
from contentstore.tests.utils import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.django import modulestore
class TestArgParsing(unittest.TestCase):
"""
Tests for parsing arguments for the 'delete_course' management command
"""
def setUp(self):
super(TestArgParsing, self).setUp()
self.command = Command()
def test_no_args(self):
"""
Testing 'delete_course' command with no arguments provided
"""
errstring = "Arguments missing: 'org/number/run commit'"
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle()
def test_no_course_key(self):
"""
Testing 'delete_course' command with no course key provided
"""
errstring = "Delete_course requires a course_key <org/number/run> argument."
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle("commit")
def test_commit_argument(self):
"""
Testing 'delete_course' command without 'commit' argument
"""
errstring = "Delete_course requires a commit argument at the end"
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle("TestX/TS01/run")
def test_invalid_course_key(self):
"""
Testing 'delete_course' command with an invalid course key argument
"""
errstring = "Invalid course_key: 'TestX/TS01'. Proper syntax: 'org/number/run commit' "
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle("TestX/TS01", "commit")
def test_missing_commit_argument(self):
"""
Testing 'delete_course' command with misspelled 'commit' argument
"""
errstring = "Delete_course requires a commit argument at the end"
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle("TestX/TS01/run", "comit")
def test_too_many_arguments(self):
"""
Testing 'delete_course' command with more than 2 arguments
"""
errstring = "Too many arguments! Expected <course_key> <commit>"
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle("TestX/TS01/run", "commit", "invalid")
class DeleteCourseTest(CourseTestCase):
"""
Test for course deleting functionality of the 'delete_course' command
@@ -82,8 +22,6 @@ class DeleteCourseTest(CourseTestCase):
def setUp(self):
super(DeleteCourseTest, self).setUp()
self.command = Command()
org = 'TestX'
course_number = 'TS01'
course_run = '2015_Q1'
@@ -95,13 +33,21 @@ class DeleteCourseTest(CourseTestCase):
run=course_run
)
def test_invalid_key_not_found(self):
"""
Test for when a course key is malformed
"""
errstring = "Invalid course_key: 'foo/TestX/TS01/2015_Q7'."
with self.assertRaisesRegexp(CommandError, errstring):
call_command('delete_course', 'foo/TestX/TS01/2015_Q7')
def test_course_key_not_found(self):
"""
Test for when a non-existing course key is entered
"""
errstring = "Course with 'TestX/TS01/2015_Q7' key not found."
with self.assertRaisesRegexp(CommandError, errstring):
self.command.handle('TestX/TS01/2015_Q7', "commit")
call_command('delete_course', 'TestX/TS01/2015_Q7')
def test_course_deleted(self):
"""
@@ -113,5 +59,5 @@ class DeleteCourseTest(CourseTestCase):
with mock.patch(self.YESNO_PATCH_LOCATION) as patched_yes_no:
patched_yes_no.return_value = True
self.command.handle('TestX/TS01/2015_Q1', "commit")
call_command('delete_course', 'TestX/TS01/2015_Q1')
self.assertIsNone(modulestore().get_course(SlashSeparatedCourseKey("TestX", "TS01", "2015_Q1")))

View File

@@ -698,7 +698,7 @@ class MiscCourseTests(ContentStoreTestCase):
self.check_components_on_page(
ADVANCED_COMPONENT_TYPES,
['Word cloud', 'Annotation', 'Text Annotation', 'Video Annotation', 'Image Annotation',
'Open Response Assessment', 'Peer Grading Interface', 'split_test'],
'split_test'],
)
@ddt.data('/Fake/asset/displayname', '\\Fake\\asset\\displayname')
@@ -1510,7 +1510,6 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
test_get_html('export_handler')
test_get_html('course_team_handler')
test_get_html('course_info_handler')
test_get_html('checklists_handler')
test_get_html('assets_handler')
test_get_html('tabs_handler')
test_get_html('settings_handler')
@@ -1694,7 +1693,6 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
self.assertEqual(course.textbooks, [])
self.assertIn('GRADER', course.grading_policy)
self.assertIn('GRADE_CUTOFFS', course.grading_policy)
self.assertGreaterEqual(len(course.checklists), 4)
# by fetching
fetched_course = self.store.get_item(course.location)
@@ -1703,8 +1701,6 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
self.assertEqual(course.start, fetched_course.start)
self.assertEqual(fetched_course.start, fetched_item.start)
self.assertEqual(course.textbooks, fetched_course.textbooks)
# is this test too strict? i.e., it requires the dicts to be ==
self.assertEqual(course.checklists, fetched_course.checklists)
def test_image_import(self):
"""Test backwards compatibilty of course image."""

View File

@@ -2,8 +2,10 @@
Test view handler for rerun (and eventually create)
"""
import ddt
from mock import patch
from django.test.client import RequestFactory
from django.core.urlresolvers import reverse
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -15,6 +17,10 @@ from student.tests.factories import UserFactory
from contentstore.tests.utils import AjaxEnabledTestClient, parse_json
from datetime import datetime
from xmodule.course_module import CourseFields
from util.organizations_helpers import (
add_organization,
get_course_organizations,
)
@ddt.ddt
@@ -33,7 +39,7 @@ class TestCourseListing(ModuleStoreTestCase):
self.factory = RequestFactory()
self.client = AjaxEnabledTestClient()
self.client.login(username=self.user.username, password='test')
self.course_create_rerun_url = reverse('course_handler')
source_course = CourseFactory.create(
org='origin',
number='the_beginning',
@@ -57,7 +63,7 @@ class TestCourseListing(ModuleStoreTestCase):
"""
Just testing the functionality the view handler adds over the tasks tested in test_clone_course
"""
response = self.client.ajax_post('/course/', {
response = self.client.ajax_post(self.course_create_rerun_url, {
'source_course_key': unicode(self.source_course_key),
'org': self.source_course_key.org, 'course': self.source_course_key.course, 'run': 'copy',
'display_name': 'not the same old name',
@@ -76,7 +82,7 @@ class TestCourseListing(ModuleStoreTestCase):
Tests newly created course has web certs enabled by default.
"""
with modulestore().default_store(store):
response = self.client.ajax_post('/course/', {
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': 'orgX',
'number': 'CS101',
'display_name': 'Course with web certs enabled',
@@ -87,3 +93,66 @@ class TestCourseListing(ModuleStoreTestCase):
new_course_key = CourseKey.from_string(data['course_key'])
course = self.store.get_course(new_course_key)
self.assertTrue(course.cert_html_view_enabled)
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': False})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_creation_without_org_app_enabled(self, store):
"""
Tests course creation workflow should not create course to org
link if organizations_app is not enabled.
"""
with modulestore().default_store(store):
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': 'orgX',
'number': 'CS101',
'display_name': 'Course with web certs enabled',
'run': '2015_T2'
})
self.assertEqual(response.status_code, 200)
data = parse_json(response)
new_course_key = CourseKey.from_string(data['course_key'])
course_orgs = get_course_organizations(new_course_key)
self.assertEqual(course_orgs, [])
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_creation_with_org_not_in_system(self, store):
"""
Tests course creation workflow when course organization does not exist
in system.
"""
with modulestore().default_store(store):
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': 'orgX',
'number': 'CS101',
'display_name': 'Course with web certs enabled',
'run': '2015_T2'
})
self.assertEqual(response.status_code, 400)
data = parse_json(response)
self.assertIn(u'Organization you selected does not exist in the system', data['error'])
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_creation_with_org_in_system(self, store):
"""
Tests course creation workflow when course organization exist in system.
"""
add_organization({
'name': 'Test Organization',
'short_name': 'orgX',
'description': 'Testing Organization Description',
})
with modulestore().default_store(store):
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': 'orgX',
'number': 'CS101',
'display_name': 'Course with web certs enabled',
'run': '2015_T2'
})
self.assertEqual(response.status_code, 200)
data = parse_json(response)
new_course_key = CourseKey.from_string(data['course_key'])
course_orgs = get_course_organizations(new_course_key)
self.assertEqual(len(course_orgs), 1)
self.assertEqual(course_orgs[0]['short_name'], 'orgX')

View File

@@ -2,63 +2,44 @@
Tests for Studio Course Settings.
"""
import datetime
import ddt
import json
import copy
import mock
from mock import patch
import unittest
from django.conf import settings
from django.utils.timezone import UTC
from django.test.utils import override_settings
from django.conf import settings
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import reverse_course_url, reverse_usage_url
from xmodule.modulestore.tests.factories import CourseFactory
from contentstore.views.component import ADVANCED_COMPONENT_POLICY_KEY
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
from models.settings.encoder import CourseSettingsEncoder
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.models.course_details import CourseDetails
from student.roles import CourseInstructorRole
from student.tests.factories import UserFactory
from models.settings.course_metadata import CourseMetadata
from xmodule.fields import Date
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.tabs import InvalidTabsException
from util.milestones_helpers import seed_milestone_relationship_types
from .utils import CourseTestCase
from xmodule.modulestore.django import modulestore
from contentstore.views.component import ADVANCED_COMPONENT_POLICY_KEY
import ddt
from xmodule.modulestore import ModuleStoreEnum
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from util.milestones_helpers import seed_milestone_relationship_types
def get_url(course_id, handler_name='settings_handler'):
return reverse_course_url(handler_name, course_id)
class CourseDetailsTestCase(CourseTestCase):
class CourseSettingsEncoderTest(CourseTestCase):
"""
Tests the first course settings page (course dates, overview, etc.).
Tests for CourseSettingsEncoder.
"""
def test_virgin_fetch(self):
details = CourseDetails.fetch(self.course.id)
self.assertEqual(details.org, self.course.location.org, "Org not copied into")
self.assertEqual(details.course_id, self.course.location.course, "Course_id not copied into")
self.assertEqual(details.run, self.course.location.name, "Course name not copied into")
self.assertEqual(details.course_image_name, self.course.course_image)
self.assertIsNotNone(details.start_date.tzinfo)
self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date))
self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start))
self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end))
self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus))
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
self.assertIsNone(details.language, "language somehow initialized" + str(details.language))
self.assertIsNone(details.has_cert_config)
self.assertFalse(details.self_paced)
def test_encoder(self):
details = CourseDetails.fetch(self.course.id)
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
@@ -87,60 +68,163 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertEquals(1, jsondetails['number'])
self.assertEqual(jsondetails['string'], 'string')
@ddt.ddt
class CourseDetailsViewTest(CourseTestCase):
"""
Tests for modifying content on the first course settings page (course dates, overview, etc.).
"""
def setUp(self):
super(CourseDetailsViewTest, self).setUp()
def alter_field(self, url, details, field, val):
"""
Change the one field to the given value and then invoke the update post to see if it worked.
"""
setattr(details, field, val)
# Need to partially serialize payload b/c the mock doesn't handle it correctly
payload = copy.copy(details.__dict__)
payload['start_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.start_date)
payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date)
payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start)
payload['enrollment_end'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_end)
resp = self.client.ajax_post(url, payload)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
@staticmethod
def convert_datetime_to_iso(datetime_obj):
"""
Use the xblock serializer to convert the datetime
"""
return Date().to_json(datetime_obj)
def test_update_and_fetch(self):
SelfPacedConfiguration(enabled=True).save()
jsondetails = CourseDetails.fetch(self.course.id)
jsondetails.syllabus = "<a href='foo'>bar</a>"
# encode - decode to convert date fields and other data which changes form
details = CourseDetails.fetch(self.course.id)
# resp s/b json from here on
url = get_url(self.course.id)
resp = self.client.get_json(url)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
utc = UTC()
self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 1, 13, 30, tzinfo=utc))
self.alter_field(url, details, 'end_date', datetime.datetime(2013, 2, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012, 10, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012, 11, 15, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'short_description', "Short Description")
self.alter_field(url, details, 'overview', "Overview")
self.alter_field(url, details, 'intro_video', "intro_video")
self.alter_field(url, details, 'effort', "effort")
self.alter_field(url, details, 'course_image_name', "course_image_name")
self.alter_field(url, details, 'language', "en")
self.alter_field(url, details, 'self_paced', "true")
def compare_details_with_encoding(self, encoded, details, context):
"""
compare all of the fields of the before and after dicts
"""
self.compare_date_fields(details, encoded, context, 'start_date')
self.compare_date_fields(details, encoded, context, 'end_date')
self.compare_date_fields(details, encoded, context, 'enrollment_start')
self.compare_date_fields(details, encoded, context, 'enrollment_end')
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).syllabus,
jsondetails.syllabus, "After set syllabus"
)
jsondetails.short_description = "Short Description"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).short_description,
jsondetails.short_description, "After set short_description"
)
jsondetails.overview = "Overview"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).overview,
jsondetails.overview, "After set overview"
)
jsondetails.intro_video = "intro_video"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).intro_video,
jsondetails.intro_video, "After set intro_video"
)
jsondetails.effort = "effort"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).effort,
jsondetails.effort, "After set effort"
)
jsondetails.self_paced = True
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).self_paced,
jsondetails.self_paced
)
jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC())
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).start_date,
jsondetails.start_date
)
jsondetails.end_date = datetime.datetime(2011, 10, 1, 0, tzinfo=UTC())
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).end_date,
jsondetails.end_date
)
jsondetails.course_image_name = "an_image.jpg"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).course_image_name,
jsondetails.course_image_name
)
jsondetails.language = "hr"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).language,
jsondetails.language
details['short_description'], encoded['short_description'], context + " short_description not =="
)
self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==")
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
self.assertEqual(details['course_image_name'], encoded['course_image_name'], context + " images not ==")
self.assertEqual(details['language'], encoded['language'], context + " languages not ==")
def compare_date_fields(self, details, encoded, context, field):
"""
Compare the given date fields between the before and after doing json deserialization
"""
if details[field] is not None:
date = Date()
if field in encoded and encoded[field] is not None:
dt1 = date.from_json(encoded[field])
dt2 = details[field]
self.assertEqual(dt1, dt2, msg="{} != {} at {}".format(dt1, dt2, context))
else:
self.fail(field + " missing from encoded but in details at " + context)
elif field in encoded and encoded[field] is not None:
self.fail(field + " included in encoding but missing from details at " + context)
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_pre_requisite_course_list_present(self):
seed_milestone_relationship_types()
settings_details_url = get_url(self.course.id)
response = self.client.get_html(settings_details_url)
self.assertContains(response, "Prerequisite Course")
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_pre_requisite_course_update_and_fetch(self):
seed_milestone_relationship_types()
url = get_url(self.course.id)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
# assert pre_requisite_courses is initialized
self.assertEqual([], course_detail_json['pre_requisite_courses'])
# update pre requisite courses with a new course keys
pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
pre_requisite_course2 = CourseFactory.create(org='edX', course='902', run='test_run')
pre_requisite_course_keys = [unicode(pre_requisite_course.id), unicode(pre_requisite_course2.id)]
course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
self.client.ajax_post(url, course_detail_json)
# fetch updated course to assert pre_requisite_courses has new values
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
self.assertEqual(pre_requisite_course_keys, course_detail_json['pre_requisite_courses'])
# remove pre requisite course
course_detail_json['pre_requisite_courses'] = []
self.client.ajax_post(url, course_detail_json)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
self.assertEqual([], course_detail_json['pre_requisite_courses'])
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_invalid_pre_requisite_course(self):
seed_milestone_relationship_types()
url = get_url(self.course.id)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
# update pre requisite courses one valid and one invalid key
pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
pre_requisite_course_keys = [unicode(pre_requisite_course.id), 'invalid_key']
course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
response = self.client.ajax_post(url, course_detail_json)
self.assertEqual(400, response.status_code)
@ddt.data(
(False, False, False),
(True, False, True),
(False, True, False),
(True, True, True),
)
def test_visibility_of_entrance_exam_section(self, feature_flags):
"""
Tests entrance exam section is available if ENTRANCE_EXAMS feature is enabled no matter any other
feature is enabled or disabled i.e ENABLE_MKTG_SITE.
"""
with patch.dict("django.conf.settings.FEATURES", {
'ENTRANCE_EXAMS': feature_flags[0],
'ENABLE_MKTG_SITE': feature_flags[1]
}):
course_details_url = get_url(self.course.id)
resp = self.client.get_html(course_details_url)
self.assertEqual(
feature_flags[2],
'<h3 id="heading-entrance-exam">' in resp.content
)
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
def test_marketing_site_fetch(self):
@@ -296,175 +380,6 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertContains(response, "Course Introduction Video")
self.assertContains(response, "Requirements")
def test_toggle_pacing_during_course_run(self):
SelfPacedConfiguration(enabled=True).save()
self.course.start = datetime.datetime.now()
modulestore().update_item(self.course, self.user.id)
details = CourseDetails.fetch(self.course.id)
updated_details = CourseDetails.update_from_json(
self.course.id,
dict(details.__dict__, self_paced=True),
self.user
)
self.assertFalse(updated_details.self_paced)
@ddt.ddt
class CourseDetailsViewTest(CourseTestCase):
"""
Tests for modifying content on the first course settings page (course dates, overview, etc.).
"""
def setUp(self):
super(CourseDetailsViewTest, self).setUp()
def alter_field(self, url, details, field, val):
"""
Change the one field to the given value and then invoke the update post to see if it worked.
"""
setattr(details, field, val)
# Need to partially serialize payload b/c the mock doesn't handle it correctly
payload = copy.copy(details.__dict__)
payload['start_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.start_date)
payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date)
payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start)
payload['enrollment_end'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_end)
resp = self.client.ajax_post(url, payload)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
@staticmethod
def convert_datetime_to_iso(datetime_obj):
"""
Use the xblock serializer to convert the datetime
"""
return Date().to_json(datetime_obj)
def test_update_and_fetch(self):
SelfPacedConfiguration(enabled=True).save()
details = CourseDetails.fetch(self.course.id)
# resp s/b json from here on
url = get_url(self.course.id)
resp = self.client.get_json(url)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
utc = UTC()
self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 1, 13, 30, tzinfo=utc))
self.alter_field(url, details, 'end_date', datetime.datetime(2013, 2, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012, 10, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012, 11, 15, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'short_description', "Short Description")
self.alter_field(url, details, 'overview', "Overview")
self.alter_field(url, details, 'intro_video', "intro_video")
self.alter_field(url, details, 'effort', "effort")
self.alter_field(url, details, 'course_image_name', "course_image_name")
self.alter_field(url, details, 'language', "en")
self.alter_field(url, details, 'self_paced', "true")
def compare_details_with_encoding(self, encoded, details, context):
"""
compare all of the fields of the before and after dicts
"""
self.compare_date_fields(details, encoded, context, 'start_date')
self.compare_date_fields(details, encoded, context, 'end_date')
self.compare_date_fields(details, encoded, context, 'enrollment_start')
self.compare_date_fields(details, encoded, context, 'enrollment_end')
self.assertEqual(details['short_description'], encoded['short_description'], context + " short_description not ==")
self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==")
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
self.assertEqual(details['course_image_name'], encoded['course_image_name'], context + " images not ==")
self.assertEqual(details['language'], encoded['language'], context + " languages not ==")
def compare_date_fields(self, details, encoded, context, field):
"""
Compare the given date fields between the before and after doing json deserialization
"""
if details[field] is not None:
date = Date()
if field in encoded and encoded[field] is not None:
dt1 = date.from_json(encoded[field])
dt2 = details[field]
self.assertEqual(dt1, dt2, msg="{} != {} at {}".format(dt1, dt2, context))
else:
self.fail(field + " missing from encoded but in details at " + context)
elif field in encoded and encoded[field] is not None:
self.fail(field + " included in encoding but missing from details at " + context)
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_pre_requisite_course_list_present(self):
seed_milestone_relationship_types()
settings_details_url = get_url(self.course.id)
response = self.client.get_html(settings_details_url)
self.assertContains(response, "Prerequisite Course")
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_pre_requisite_course_update_and_fetch(self):
seed_milestone_relationship_types()
url = get_url(self.course.id)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
# assert pre_requisite_courses is initialized
self.assertEqual([], course_detail_json['pre_requisite_courses'])
# update pre requisite courses with a new course keys
pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
pre_requisite_course2 = CourseFactory.create(org='edX', course='902', run='test_run')
pre_requisite_course_keys = [unicode(pre_requisite_course.id), unicode(pre_requisite_course2.id)]
course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
self.client.ajax_post(url, course_detail_json)
# fetch updated course to assert pre_requisite_courses has new values
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
self.assertEqual(pre_requisite_course_keys, course_detail_json['pre_requisite_courses'])
# remove pre requisite course
course_detail_json['pre_requisite_courses'] = []
self.client.ajax_post(url, course_detail_json)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
self.assertEqual([], course_detail_json['pre_requisite_courses'])
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_invalid_pre_requisite_course(self):
seed_milestone_relationship_types()
url = get_url(self.course.id)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
# update pre requisite courses one valid and one invalid key
pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
pre_requisite_course_keys = [unicode(pre_requisite_course.id), 'invalid_key']
course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
response = self.client.ajax_post(url, course_detail_json)
self.assertEqual(400, response.status_code)
@ddt.data(
(False, False, False),
(True, False, True),
(False, True, False),
(True, True, True),
)
def test_visibility_of_entrance_exam_section(self, feature_flags):
"""
Tests entrance exam section is available if ENTRANCE_EXAMS feature is enabled no matter any other
feature is enabled or disabled i.e ENABLE_MKTG_SITE.
"""
with patch.dict("django.conf.settings.FEATURES", {
'ENTRANCE_EXAMS': feature_flags[0],
'ENABLE_MKTG_SITE': feature_flags[1]
}):
course_details_url = get_url(self.course.id)
resp = self.client.get_html(course_details_url)
self.assertEqual(
feature_flags[2],
'<h3 id="heading-entrance-exam">' in resp.content
)
@ddt.ddt
class CourseGradingTest(CourseTestCase):
@@ -856,7 +771,7 @@ class CourseMetadataEditingTest(CourseTestCase):
{
"advertised_start": {"value": "start A"},
"days_early_for_beta": {"value": 2},
"advanced_modules": {"value": ['combinedopenended']},
"advanced_modules": {"value": ['notes']},
},
user=self.user
)
@@ -866,7 +781,7 @@ class CourseMetadataEditingTest(CourseTestCase):
# Tab gets tested in test_advanced_settings_munge_tabs
self.assertIn('advanced_modules', test_model, 'Missing advanced_modules')
self.assertEqual(test_model['advanced_modules']['value'], ['combinedopenended'], 'advanced_module is not updated')
self.assertEqual(test_model['advanced_modules']['value'], ['notes'], 'advanced_module is not updated')
def test_validate_from_json_wrong_inputs(self):
# input incorrectly formatted data
@@ -990,48 +905,21 @@ class CourseMetadataEditingTest(CourseTestCase):
"""
Test that adding and removing specific advanced components adds and removes tabs.
"""
open_ended_tab = {"type": "open_ended", "name": "Open Ended Panel"}
peer_grading_tab = {"type": "peer_grading", "name": "Peer grading"}
# First ensure that none of the tabs are visible
self.assertNotIn(open_ended_tab, self.course.tabs)
self.assertNotIn(peer_grading_tab, self.course.tabs)
self.assertNotIn(self.notes_tab, self.course.tabs)
# Now add the "combinedopenended" component and verify that the tab has been added
self.client.ajax_post(self.course_setting_url, {
ADVANCED_COMPONENT_POLICY_KEY: {"value": ["combinedopenended"]}
})
course = modulestore().get_course(self.course.id)
self.assertIn(open_ended_tab, course.tabs)
self.assertIn(peer_grading_tab, course.tabs)
self.assertNotIn(self.notes_tab, course.tabs)
# Now enable student notes and verify that the "My Notes" tab has also been added
self.client.ajax_post(self.course_setting_url, {
ADVANCED_COMPONENT_POLICY_KEY: {"value": ["combinedopenended", "notes"]}
})
course = modulestore().get_course(self.course.id)
self.assertIn(open_ended_tab, course.tabs)
self.assertIn(peer_grading_tab, course.tabs)
self.assertIn(self.notes_tab, course.tabs)
# Now remove the "combinedopenended" component and verify that the tab is gone
# Now enable student notes and verify that the "My Notes" tab has been added
self.client.ajax_post(self.course_setting_url, {
ADVANCED_COMPONENT_POLICY_KEY: {"value": ["notes"]}
})
course = modulestore().get_course(self.course.id)
self.assertNotIn(open_ended_tab, course.tabs)
self.assertNotIn(peer_grading_tab, course.tabs)
self.assertIn(self.notes_tab, course.tabs)
# Finally disable student notes and verify that the "My Notes" tab is gone
# Disable student notes and verify that the "My Notes" tab is gone
self.client.ajax_post(self.course_setting_url, {
ADVANCED_COMPONENT_POLICY_KEY: {"value": [""]}
})
course = modulestore().get_course(self.course.id)
self.assertNotIn(open_ended_tab, course.tabs)
self.assertNotIn(peer_grading_tab, course.tabs)
self.assertNotIn(self.notes_tab, course.tabs)
def test_advanced_components_munge_tabs_validation_failure(self):
@@ -1067,40 +955,6 @@ class CourseMetadataEditingTest(CourseTestCase):
tab_list.append(self.notes_tab)
self.assertEqual(tab_list, course.tabs)
@override_settings(FEATURES={'CERTIFICATES_HTML_VIEW': True})
def test_web_view_certifcate_configuration_settings(self):
"""
Test that has_cert_config is updated based on cert_html_view_enabled setting.
"""
test_model = CourseMetadata.update_from_json(
self.course,
{
"cert_html_view_enabled": {"value": "true"}
},
user=self.user
)
self.assertIn('cert_html_view_enabled', test_model)
url = get_url(self.course.id)
response = self.client.get_json(url)
course_detail_json = json.loads(response.content)
self.assertFalse(course_detail_json['has_cert_config'])
# Now add a certificate configuration
certificates = [
{
'id': 1,
'name': 'Certificate Config Name',
'course_title': 'Title override',
'signatories': [],
'is_active': True
}
]
self.course.certificates = {'certificates': certificates}
modulestore().update_item(self.course, self.user.id)
response = self.client.get_json(url)
course_detail_json = json.loads(response.content)
self.assertTrue(course_detail_json['has_cert_config'])
class CourseGraderUpdatesTest(CourseTestCase):
"""

View File

@@ -15,6 +15,7 @@ from unittest import skip
from django.conf import settings
from course_modes.models import CourseMode
from openedx.core.djangoapps.models.course_details import CourseDetails
from xmodule.library_tools import normalize_key_for_search
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import SignalHandler, modulestore
@@ -192,26 +193,6 @@ class MixedWithOptionsTestCase(MixedSplitTestCase):
with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
store.update_item(item, ModuleStoreEnum.UserID.test)
def update_about_item(self, store, about_key, data):
"""
Update the about item with the new data blob. If data is None, then
delete the about item.
"""
temploc = self.course.id.make_usage_key('about', about_key)
if data is None:
try:
self.delete_item(store, temploc)
# Ignore an attempt to delete an item that doesn't exist
except ValueError:
pass
else:
try:
about_item = store.get_item(temploc)
except ItemNotFoundError:
about_item = store.create_xblock(self.course.runtime, self.course.id, 'about', about_key)
about_item.data = data
store.update_item(about_item, ModuleStoreEnum.UserID.test, allow_not_found=True)
@ddt.ddt
class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
@@ -487,7 +468,9 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
def _test_course_about_store_index(self, store):
""" Test that informational properties in the about store end up in the course_info index """
short_description = "Not just anybody"
self.update_about_item(store, "short_description", short_description)
CourseDetails.update_about_item(
self.course, "short_description", short_description, ModuleStoreEnum.UserID.test, store
)
self.reindex_course(store)
response = self.searcher.search(
doc_type=CourseAboutSearchIndexer.DISCOVERY_DOCUMENT_TYPE,

View File

@@ -219,26 +219,6 @@ class ContentStoreImportTest(SignalDisconnectTestMixin, ModuleStoreTestCase):
conditional_module.show_tag_list
)
def test_rewrite_reference(self):
module_store = modulestore()
target_id = module_store.make_course_key('testX', 'peergrading_copy', 'copy_run')
import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
['open_ended'],
target_id=target_id,
create_if_not_present=True
)
peergrading_module = module_store.get_item(
target_id.make_usage_key('peergrading', 'PeerGradingLinked')
)
self.assertIsNotNone(peergrading_module)
self.assertEqual(
target_id.make_usage_key('combinedopenended', 'SampleQuestion'),
peergrading_module.link_to_location
)
def test_rewrite_reference_value_dict_published(self):
"""
Test rewriting references in ReferenceValueDict, specifically with published content.

View File

@@ -387,7 +387,7 @@ class TestLibraries(LibraryTestCase):
html_block = modulestore().get_item(lc_block.children[0])
self.assertEqual(html_block.data, data2)
@patch("xmodule.library_tools.SearchEngine.get_search_engine", Mock(return_value=None))
@patch("xmodule.library_tools.SearchEngine.get_search_engine", Mock(return_value=None, autospec=True))
def test_refreshes_children_if_capa_type_change(self):
""" Tests that children are automatically refreshed if capa type field changes """
name1, name2 = "Option Problem", "Multiple Choice Problem"

View File

@@ -110,55 +110,6 @@ class ExtraPanelTabTestCase(TestCase):
return course
@ddt.ddt
class CourseImageTestCase(ModuleStoreTestCase):
"""Tests for course image URLs."""
def verify_url(self, expected_url, actual_url):
"""
Helper method for verifying the URL is as expected.
"""
if not expected_url.startswith("/"):
expected_url = "/" + expected_url
self.assertEquals(expected_url, actual_url)
def test_get_image_url(self):
"""Test image URL formatting."""
course = CourseFactory.create()
self.verify_url(
unicode(course.id.make_asset_key('asset', course.course_image)),
utils.course_image_url(course)
)
def test_non_ascii_image_name(self):
""" Verify that non-ascii image names are cleaned """
course_image = u'before_\N{SNOWMAN}_after.jpg'
course = CourseFactory.create(course_image=course_image)
self.verify_url(
unicode(course.id.make_asset_key('asset', course_image.replace(u'\N{SNOWMAN}', '_'))),
utils.course_image_url(course)
)
def test_spaces_in_image_name(self):
""" Verify that image names with spaces in them are cleaned """
course_image = u'before after.jpg'
course = CourseFactory.create(course_image=u'before after.jpg')
self.verify_url(
unicode(course.id.make_asset_key('asset', course_image.replace(" ", "_"))),
utils.course_image_url(course)
)
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
def test_empty_image_name(self, default_store):
""" Verify that empty image names are cleaned """
course_image = u''
course = CourseFactory.create(course_image=course_image, default_store=default_store)
self.assertEquals(
course_image,
utils.course_image_url(course),
)
class XBlockVisibilityTestCase(ModuleStoreTestCase):
"""Tests for xblock visibility for students."""

View File

@@ -3,7 +3,6 @@ Common utility functions useful throughout the contentstore
"""
import logging
from opaque_keys import InvalidKeyError
import re
from datetime import datetime
from pytz import UTC
@@ -14,7 +13,6 @@ from django.utils.translation import ugettext as _
from django_comment_common.models import assign_default_role
from django_comment_common.utils import seed_permissions_roles
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
@@ -158,16 +156,6 @@ def get_lms_link_for_certificate_web_view(user_id, course_key, mode):
)
def course_image_url(course):
"""Returns the image url for the course."""
try:
loc = StaticContent.compute_location(course.location.course_key, course.course_image)
except InvalidKeyError:
return ''
path = StaticContent.serialize_asset_key_with_slash(loc)
return path
# pylint: disable=invalid-name
def is_currently_visible_to_students(xblock):
"""
@@ -314,25 +302,6 @@ def reverse_usage_url(handler_name, usage_key, kwargs=None):
return reverse_url(handler_name, 'usage_key_string', usage_key, kwargs)
def has_active_web_certificate(course):
"""
Returns True if given course has active web certificate configuration.
If given course has no active web certificate configuration returns False.
Returns None If `CERTIFICATES_HTML_VIEW` is not enabled of course has not enabled
`cert_html_view_enabled` settings.
"""
cert_config = None
if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False) and course.cert_html_view_enabled:
cert_config = False
certificates = getattr(course, 'certificates', {})
configurations = certificates.get('certificates', [])
for config in configurations:
if config.get('is_active'):
cert_config = True
break
return cert_config
def get_user_partition_info(xblock, schemes=None, course=None):
"""
Retrieve user partition information for an XBlock for display in editors.

View File

@@ -5,7 +5,6 @@
# Disable warnings about import from wildcard
# All files below declare exports with __all__
from .assets import *
from .checklist import *
from .component import *
from .course import *
from .entrance_exam import *

View File

@@ -1,142 +0,0 @@
import json
import copy
from util.json_request import JsonResponse
from django.http import HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import ensure_csrf_cookie
from edxmako.shortcuts import render_to_response
from django.http import HttpResponseNotFound
from django.core.exceptions import PermissionDenied
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore
from contentstore.utils import reverse_course_url
from student.auth import has_course_author_access
from xmodule.course_module import CourseDescriptor
from django.utils.translation import ugettext
__all__ = ['checklists_handler']
@require_http_methods(("GET", "POST", "PUT"))
@login_required
@ensure_csrf_cookie
def checklists_handler(request, course_key_string, checklist_index=None):
"""
The restful handler for checklists.
GET
html: return html page for all checklists
json: return json representing all checklists. checklist_index is not supported for GET at this time.
POST or PUT
json: updates the checked state for items within a particular checklist. checklist_index is required.
"""
course_key = CourseKey.from_string(course_key_string)
if not has_course_author_access(request.user, course_key):
raise PermissionDenied()
course_module = modulestore().get_course(course_key)
json_request = 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json')
if request.method == 'GET':
# If course was created before checklists were introduced, copy them over
# from the template.
if not course_module.checklists:
course_module.checklists = CourseDescriptor.checklists.default
modulestore().update_item(course_module, request.user.id)
expanded_checklists = expand_all_action_urls(course_module)
if json_request:
return JsonResponse(expanded_checklists)
else:
handler_url = reverse_course_url('checklists_handler', course_key)
return render_to_response('checklists.html',
{
'handler_url': handler_url,
# context_course is used by analytics
'context_course': course_module,
'checklists': expanded_checklists
})
elif json_request:
# Can now assume POST or PUT because GET handled above.
if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
index = int(checklist_index)
persisted_checklist = course_module.checklists[index]
modified_checklist = json.loads(request.body)
# Only thing the user can modify is the "checked" state.
# We don't want to persist what comes back from the client because it will
# include the expanded action URLs (which are non-portable).
for item_index, item in enumerate(modified_checklist.get('items')):
persisted_checklist['items'][item_index]['is_checked'] = item['is_checked']
# seeming noop which triggers kvs to record that the metadata is
# not default
course_module.checklists = course_module.checklists
course_module.save()
modulestore().update_item(course_module, request.user.id)
expanded_checklist = expand_checklist_action_url(course_module, persisted_checklist)
return JsonResponse(localize_checklist_text(expanded_checklist))
else:
return HttpResponseBadRequest(
("Could not save checklist state because the checklist index "
"was out of range or unspecified."),
content_type="text/plain"
)
else:
return HttpResponseNotFound()
def expand_all_action_urls(course_module):
"""
Gets the checklists out of the course module and expands their action urls.
Returns a copy of the checklists with modified urls, without modifying the persisted version
of the checklists.
"""
expanded_checklists = []
for checklist in course_module.checklists:
expanded_checklists.append(localize_checklist_text(expand_checklist_action_url(course_module, checklist)))
return expanded_checklists
def expand_checklist_action_url(course_module, checklist):
"""
Expands the action URLs for a given checklist and returns the modified version.
The method does a copy of the input checklist and does not modify the input argument.
"""
expanded_checklist = copy.deepcopy(checklist)
urlconf_map = {
"ManageUsers": "course_team_handler",
"CourseOutline": "course_handler",
"SettingsDetails": "settings_handler",
"SettingsGrading": "grading_handler",
}
for item in expanded_checklist.get('items'):
action_url = item.get('action_url')
if action_url in urlconf_map:
item['action_url'] = reverse_course_url(urlconf_map[action_url], course_module.id)
return expanded_checklist
def localize_checklist_text(checklist):
"""
Localize texts for a given checklist and returns the modified version.
The method does an in-place operation so the input checklist is modified directly.
"""
# Localize checklist name
checklist['short_description'] = ugettext(checklist['short_description']) # pylint: disable=translation-of-non-string
# Localize checklist items
for item in checklist.get('items'):
item['short_description'] = ugettext(item['short_description']) # pylint: disable=translation-of-non-string
item['long_description'] = ugettext(item['long_description']) # pylint: disable=translation-of-non-string
item['action_text'] = ugettext(item['action_text']) # pylint: disable=translation-of-non-string
return checklist

View File

@@ -30,11 +30,11 @@ from student.auth import has_course_author_access
from django.utils.translation import ugettext as _
from models.settings.course_grading import CourseGradingModel
__all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'ADVANCED_COMPONENT_POLICY_KEY',
'container_handler',
'component_handler'
]
__all__ = [
'ADVANCED_COMPONENT_POLICY_KEY',
'container_handler',
'component_handler'
]
log = logging.getLogger(__name__)
@@ -43,7 +43,6 @@ COMPONENT_TYPES = ['discussion', 'html', 'problem', 'video']
# Constants for determining if these components should be enabled for this course
SPLIT_TEST_COMPONENT_TYPE = 'split_test'
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
NOTE_COMPONENT_TYPES = ['notes']
if settings.FEATURES.get('ALLOW_ALL_ADVANCED_COMPONENTS'):

View File

@@ -58,16 +58,18 @@ from course_action_state.models import CourseRerunState, CourseRerunUIStateManag
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from edxmako.shortcuts import render_to_response
from microsite_configuration import microsite
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
from models.settings.encoder import CourseSettingsEncoder
from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors
from openedx.core.djangoapps.credit.api import is_credit_course, get_credit_requirements
from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements
from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.utils import get_programs
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.lib.course_tabs import CourseTabPluginManager
from openedx.core.lib.courses import course_image_url
from openedx.core.lib.js_utils import escape_json_dumps
from student import auth
from student.auth import has_course_author_access, has_studio_write_access, has_studio_read_access
@@ -82,6 +84,11 @@ from util.milestones_helpers import (
is_valid_course_key,
set_prerequisite_courses,
)
from util.organizations_helpers import (
add_organization_course,
get_organization_by_short_name,
organizations_enabled,
)
from util.string_utils import _has_non_ascii_characters
from xmodule.contentstore.content import StaticContent
from xmodule.course_module import CourseFields
@@ -736,8 +743,17 @@ def _create_new_course(request, org, number, run, fields):
Returns the URL for the course overview page.
Raises DuplicateCourseError if the course already exists
"""
org_data = get_organization_by_short_name(org)
if not org_data and organizations_enabled():
return JsonResponse(
{'error': _('You must link this course to an organization in order to continue. '
'Organization you selected does not exist in the system, '
'you will need to add it to the system')},
status=400
)
store_for_new_course = modulestore().default_modulestore.get_modulestore_type()
new_course = create_new_course_in_store(store_for_new_course, request.user, org, number, run, fields)
add_organization_course(org_data, new_course.id)
return JsonResponse({
'url': reverse_course_url('course_handler', new_course.id),
'course_key': unicode(new_course.id),
@@ -939,7 +955,7 @@ def settings_handler(request, course_key_string):
'context_course': course_module,
'course_locator': course_key,
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key),
'course_image_url': utils.course_image_url(course_module),
'course_image_url': course_image_url(course_module),
'details_url': reverse_course_url('settings_handler', course_key),
'about_page_editable': about_page_editable,
'short_description_editable': short_description_editable,

View File

@@ -0,0 +1,23 @@
"""Organizations views for use with Studio."""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import View
from django.http import HttpResponse
from openedx.core.lib.js_utils import escape_json_dumps
from util.organizations_helpers import get_organizations
class OrganizationListView(View):
"""View rendering organization list as json.
This view renders organization list json which is used in org
autocomplete while creating new course.
"""
@method_decorator(login_required)
def get(self, request, *args, **kwargs):
"""Returns organization list as json."""
organizations = get_organizations()
org_names_list = [(org["short_name"]) for org in organizations]
return HttpResponse(escape_json_dumps(org_names_list), content_type='application/json; charset=utf-8')

View File

@@ -1,12 +1,13 @@
"""Programs views for use with Studio."""
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.http import Http404
from django.http import Http404, JsonResponse
from django.utils.decorators import method_decorator
from django.views.generic import View
from edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.lib.token_utils import get_id_token
class ProgramAuthoringView(View):
@@ -18,13 +19,6 @@ class ProgramAuthoringView(View):
"""
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
"""Relays requests to matching methods.
Decorated to require login before accessing the authoring app.
"""
return super(ProgramAuthoringView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
"""Populate the template context with values required for the authoring app to run."""
programs_config = ProgramsApiConfig.current()
@@ -34,7 +28,21 @@ class ProgramAuthoringView(View):
'show_programs_header': programs_config.is_studio_tab_enabled,
'authoring_app_config': programs_config.authoring_app_config,
'programs_api_url': programs_config.public_api_url,
'programs_token_url': reverse('programs_id_token'),
'studio_home_url': reverse('home'),
})
else:
raise Http404
class ProgramsIdTokenView(View):
"""Provides id tokens to JavaScript clients for use with the Programs API."""
@method_decorator(login_required)
def get(self, request, *args, **kwargs):
"""Generate and return a token, if the integration is enabled."""
if ProgramsApiConfig.current().is_studio_tab_enabled:
id_token = get_id_token(request.user, 'programs')
return JsonResponse({'id_token': id_token})
else:
raise Http404

View File

@@ -1,153 +0,0 @@
""" Unit tests for checklist methods in views.py. """
from contentstore.utils import reverse_course_url
from contentstore.views.checklist import expand_checklist_action_url
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.django import modulestore
import json
from contentstore.tests.utils import CourseTestCase
class ChecklistTestCase(CourseTestCase):
""" Test for checklist get and put methods. """
def setUp(self):
""" Creates the test course. """
super(ChecklistTestCase, self).setUp()
self.course = CourseFactory.create(org='mitX', number='333', display_name='Checklists Course')
self.checklists_url = self.get_url()
def get_url(self, checklist_index=None):
url_args = {'checklist_index': checklist_index} if checklist_index else None
return reverse_course_url('checklists_handler', self.course.id, kwargs=url_args)
def get_persisted_checklists(self):
""" Returns the checklists as persisted in the modulestore. """
return modulestore().get_item(self.course.location).checklists
def compare_checklists(self, persisted, request):
"""
Handles url expansion as possible difference and descends into guts
"""
self.assertEqual(persisted['short_description'], request['short_description'])
expanded_checklist = expand_checklist_action_url(self.course, persisted)
for pers, req in zip(expanded_checklist['items'], request['items']):
self.assertEqual(pers['short_description'], req['short_description'])
self.assertEqual(pers['long_description'], req['long_description'])
self.assertEqual(pers['is_checked'], req['is_checked'])
self.assertEqual(pers['action_url'], req['action_url'])
self.assertEqual(pers['action_text'], req['action_text'])
self.assertEqual(pers['action_external'], req['action_external'])
def test_get_checklists(self):
""" Tests the get checklists method and URL expansion. """
response = self.client.get(self.checklists_url)
self.assertContains(response, "Getting Started With Studio")
# Verify expansion of action URL happened.
self.assertContains(response, 'course_team/mitX/333/Checklists_Course')
# Verify persisted checklist does NOT have expanded URL.
checklist_0 = self.get_persisted_checklists()[0]
self.assertEqual('ManageUsers', get_action_url(checklist_0, 0))
payload = response.content
# Now delete the checklists from the course and verify they get repopulated (for courses
# created before checklists were introduced).
self.course.checklists = None
# Save the changed `checklists` to the underlying KeyValueStore before updating the modulestore
self.course.save()
modulestore().update_item(self.course, self.user.id)
self.assertEqual(self.get_persisted_checklists(), None)
response = self.client.get(self.checklists_url)
self.assertEqual(payload, response.content)
def test_get_checklists_html(self):
""" Tests getting the HTML template for the checklists page). """
response = self.client.get(self.checklists_url, HTTP_ACCEPT='text/html')
self.assertContains(response, "Getting Started With Studio")
# The HTML generated will define the handler URL (for use by the Backbone model).
self.assertContains(response, self.checklists_url)
def test_update_checklists_no_index(self):
""" No checklist index, should return all of them. """
returned_checklists = json.loads(self.client.get(self.checklists_url).content)
# Verify that persisted checklists do not have expanded action URLs.
# compare_checklists will verify that returned_checklists DO have expanded action URLs.
pers = self.get_persisted_checklists()
self.assertEqual('CourseOutline', get_first_item(pers[1]).get('action_url'))
for pay, resp in zip(pers, returned_checklists):
self.compare_checklists(pay, resp)
def test_update_checklists_index_ignored_on_get(self):
""" Checklist index ignored on get. """
update_url = self.get_url(1)
returned_checklists = json.loads(self.client.get(update_url).content)
for pay, resp in zip(self.get_persisted_checklists(), returned_checklists):
self.compare_checklists(pay, resp)
def test_update_checklists_post_no_index(self):
""" No checklist index, will error on post. """
response = self.client.post(self.checklists_url)
self.assertContains(response, 'Could not save checklist', status_code=400)
def test_update_checklists_index_out_of_range(self):
""" Checklist index out of range, will error on post. """
update_url = self.get_url(100)
response = self.client.post(update_url)
self.assertContains(response, 'Could not save checklist', status_code=400)
def test_update_checklists_index(self):
""" Check that an update of a particular checklist works. """
update_url = self.get_url(1)
payload = self.course.checklists[1]
self.assertFalse(get_first_item(payload).get('is_checked'))
self.assertEqual('CourseOutline', get_first_item(payload).get('action_url'))
get_first_item(payload)['is_checked'] = True
returned_checklist = json.loads(self.client.ajax_post(update_url, payload).content)
self.assertTrue(get_first_item(returned_checklist).get('is_checked'))
persisted_checklist = self.get_persisted_checklists()[1]
# Verify that persisted checklist does not have expanded action URLs.
# compare_checklists will verify that returned_checklist DOES have expanded action URLs.
self.assertEqual('CourseOutline', get_first_item(persisted_checklist).get('action_url'))
self.compare_checklists(persisted_checklist, returned_checklist)
def test_update_checklists_delete_unsupported(self):
""" Delete operation is not supported. """
update_url = self.get_url(100)
response = self.client.delete(update_url)
self.assertEqual(response.status_code, 405)
def test_expand_checklist_action_url(self):
"""
Tests the method to expand checklist action url.
"""
def test_expansion(checklist, index, stored, expanded):
"""
Tests that the expected expanded value is returned for the item at the given index.
Also verifies that the original checklist is not modified.
"""
self.assertEqual(get_action_url(checklist, index), stored)
expanded_checklist = expand_checklist_action_url(self.course, checklist)
self.assertEqual(get_action_url(expanded_checklist, index), expanded)
# Verify no side effect in the original list.
self.assertEqual(get_action_url(checklist, index), stored)
test_expansion(self.course.checklists[0], 0, 'ManageUsers', '/course_team/mitX/333/Checklists_Course')
test_expansion(self.course.checklists[1], 1, 'CourseOutline', '/course/mitX/333/Checklists_Course')
test_expansion(self.course.checklists[2], 0, 'http://help.edge.edx.org/', 'http://help.edge.edx.org/')
def get_first_item(checklist):
""" Returns the first item from the checklist. """
return checklist['items'][0]
def get_action_url(checklist, index):
"""
Returns the action_url for the item at the specified index in the given checklist.
"""
return checklist['items'][index]['action_url']

View File

@@ -10,6 +10,7 @@ import pytz
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.test.utils import override_settings
from django.utils.translation import ugettext as _
from contentstore.courseware_index import CoursewareSearchIndexer, SearchIndexingError
@@ -440,6 +441,7 @@ class TestCourseOutline(CourseTestCase):
info['block_types_enabled'],
any(component in advanced_modules for component in deprecated_block_types)
)
self.assertItemsEqual(info['blocks'], expected_blocks)
self.assertEqual(
info['advance_settings_url'],
@@ -455,27 +457,29 @@ class TestCourseOutline(CourseTestCase):
"""
Verify deprecated warning info for single deprecated feature.
"""
block_types = settings.DEPRECATED_BLOCK_TYPES
course_module = modulestore().get_item(self.course.location)
self._create_test_data(course_module, create_blocks=True, block_types=block_types, publish=publish)
info = _deprecated_blocks_info(course_module, block_types)
self._verify_deprecated_info(
course_module.id,
course_module.advanced_modules,
info,
block_types
)
block_types = ['notes']
with override_settings(DEPRECATED_BLOCK_TYPES=block_types):
course_module = modulestore().get_item(self.course.location)
self._create_test_data(course_module, create_blocks=True, block_types=block_types, publish=publish)
info = _deprecated_blocks_info(course_module, block_types)
self._verify_deprecated_info(
course_module.id,
course_module.advanced_modules,
info,
block_types
)
def test_verify_deprecated_warning_message_with_multiple_features(self):
"""
Verify deprecated warning info for multiple deprecated features.
"""
block_types = ['peergrading', 'combinedopenended', 'openassessment']
course_module = modulestore().get_item(self.course.location)
self._create_test_data(course_module, create_blocks=True, block_types=block_types)
block_types = ['notes', 'lti']
with override_settings(DEPRECATED_BLOCK_TYPES=block_types):
course_module = modulestore().get_item(self.course.location)
self._create_test_data(course_module, create_blocks=True, block_types=block_types)
info = _deprecated_blocks_info(course_module, block_types)
self._verify_deprecated_info(course_module.id, course_module.advanced_modules, info, block_types)
info = _deprecated_blocks_info(course_module, block_types)
self._verify_deprecated_info(course_module.id, course_module.advanced_modules, info, block_types)
@ddt.data(
{'delete_vertical': True},
@@ -492,7 +496,7 @@ class TestCourseOutline(CourseTestCase):
un-published block(s). This behavior should be same if we delete
unpublished vertical or problem.
"""
block_types = ['peergrading']
block_types = ['notes']
course_module = modulestore().get_item(self.course.location)
vertical1 = ItemFactory.create(
@@ -500,8 +504,8 @@ class TestCourseOutline(CourseTestCase):
)
problem1 = ItemFactory.create(
parent_location=vertical1.location,
category='peergrading',
display_name='peergrading problem in vert1',
category='notes',
display_name='notes problem in vert1',
publish_item=False
)
@@ -515,8 +519,8 @@ class TestCourseOutline(CourseTestCase):
)
ItemFactory.create(
parent_location=vertical2.location,
category='peergrading',
display_name='peergrading problem in vert2',
category='notes',
display_name='notes problem in vert2',
pubish_item=True
)
# At this point CourseStructure will contain both the above
@@ -526,8 +530,8 @@ class TestCourseOutline(CourseTestCase):
self.assertItemsEqual(
info['blocks'],
[
[reverse_usage_url('container_handler', vertical1.location), 'peergrading problem in vert1'],
[reverse_usage_url('container_handler', vertical2.location), 'peergrading problem in vert2']
[reverse_usage_url('container_handler', vertical1.location), 'notes problem in vert1'],
[reverse_usage_url('container_handler', vertical2.location), 'notes problem in vert2']
]
)
@@ -542,7 +546,7 @@ class TestCourseOutline(CourseTestCase):
# There shouldn't be any info present about un-published vertical1
self.assertEqual(
info['blocks'],
[[reverse_usage_url('container_handler', vertical2.location), 'peergrading problem in vert2']]
[[reverse_usage_url('container_handler', vertical2.location), 'notes problem in vert2']]
)

View File

@@ -1388,28 +1388,28 @@ class TestComponentTemplates(CourseTestCase):
Test the handling of advanced problem templates.
"""
problem_templates = self.get_templates_of_type('problem')
ora_template = self.get_template(problem_templates, u'Peer Assessment')
self.assertIsNotNone(ora_template)
self.assertEqual(ora_template.get('category'), 'openassessment')
self.assertIsNone(ora_template.get('boilerplate_name', None))
circuit_template = self.get_template(problem_templates, u'Circuit Schematic Builder')
self.assertIsNotNone(circuit_template)
self.assertEqual(circuit_template.get('category'), 'problem')
self.assertEqual(circuit_template.get('boilerplate_name'), 'circuitschematic.yaml')
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ["combinedopenended", "peergrading"])
def test_ora1_no_advance_component_button(self):
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ["poll", "survey"])
def test_deprecated_no_advance_component_button(self):
"""
Test that there will be no `Advanced` button on unit page if `combinedopenended` and `peergrading` are
deprecated provided that there are only 'combinedopenended', 'peergrading' modules in `Advanced Module List`
Test that there will be no `Advanced` button on unit page if units are
deprecated provided that they are the only modules in `Advanced Module List`
"""
self.course.advanced_modules.extend(['combinedopenended', 'peergrading'])
self.course.advanced_modules.extend(['poll', 'survey'])
templates = get_component_templates(self.course)
button_names = [template['display_name'] for template in templates]
self.assertNotIn('Advanced', button_names)
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ["combinedopenended", "peergrading"])
def test_cannot_create_ora1_problems(self):
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ["poll", "survey"])
def test_cannot_create_deprecated_problems(self):
"""
Test that we can't create ORA1 problems if `combinedopenended` and `peergrading` are deprecated
Test that we can't create problems if they are deprecated
"""
self.course.advanced_modules.extend(['annotatable', 'combinedopenended', 'peergrading'])
self.course.advanced_modules.extend(['annotatable', 'poll', 'survey'])
templates = get_component_templates(self.course)
button_names = [template['display_name'] for template in templates]
self.assertIn('Advanced', button_names)
@@ -1418,17 +1418,17 @@ class TestComponentTemplates(CourseTestCase):
self.assertEqual(template_display_names, ['Annotation'])
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', [])
def test_create_ora1_problems(self):
def test_create_non_deprecated_problems(self):
"""
Test that we can create ORA1 problems if `combinedopenended` and `peergrading` are not deprecated
Test that we can create problems if they are not deprecated
"""
self.course.advanced_modules.extend(['annotatable', 'combinedopenended', 'peergrading'])
self.course.advanced_modules.extend(['annotatable', 'poll', 'survey'])
templates = get_component_templates(self.course)
button_names = [template['display_name'] for template in templates]
self.assertIn('Advanced', button_names)
self.assertEqual(len(templates[0]['templates']), 3)
template_display_names = [template['display_name'] for template in templates[0]['templates']]
self.assertEqual(template_display_names, ['Annotation', 'Open Response Assessment', 'Peer Grading Interface'])
self.assertEqual(template_display_names, ['Annotation', 'Poll', 'Survey'])
@ddt.ddt

View File

@@ -0,0 +1,32 @@
"""Tests covering the Organizations listing on the Studio home."""
import json
from mock import patch
from django.core.urlresolvers import reverse
from django.test import TestCase
from util.organizations_helpers import add_organization
from student.tests.factories import UserFactory
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True})
class TestOrganizationListing(TestCase):
"""Verify Organization listing behavior."""
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True})
def setUp(self):
super(TestOrganizationListing, self).setUp()
self.staff = UserFactory(is_staff=True)
self.client.login(username=self.staff.username, password='test')
self.org_names_listing_url = reverse('organizations')
self.org_short_names = ["alphaX", "betaX", "orgX"]
for index, short_name in enumerate(self.org_short_names):
add_organization(organization_data={
'name': 'Test Organization %s' % index,
'short_name': short_name,
'description': 'Testing Organization %s Description' % index,
})
def test_organization_list(self):
"""Verify that the organization names list api returns list of organization short names."""
response = self.client.get(self.org_names_listing_url, HTTP_ACCEPT='application/json')
self.assertEqual(response.status_code, 200)
org_names = json.loads(response.content)
self.assertEqual(org_names, self.org_short_names)

View File

@@ -1,17 +1,20 @@
"""Tests covering the Programs listing on the Studio home."""
import json
from django.conf import settings
from django.core.urlresolvers import reverse
import httpretty
import mock
from oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreTestCase):
class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModuleStoreTestCase):
"""Verify Program listing behavior."""
def setUp(self):
super(TestProgramListing, self).setUp()
@@ -70,7 +73,7 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreT
self.assertIn(program_name, response.content)
class TestProgramAuthoringView(ProgramsApiConfigMixin, ModuleStoreTestCase):
class TestProgramAuthoringView(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
"""Verify the behavior of the program authoring app's host view."""
def setUp(self):
super(TestProgramAuthoringView, self).setUp()
@@ -118,3 +121,43 @@ class TestProgramAuthoringView(ProgramsApiConfigMixin, ModuleStoreTestCase):
student = UserFactory(is_staff=False)
self.client.login(username=student.username, password='test')
self._assert_status(404)
class TestProgramsIdTokenView(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
"""Tests for the programs id_token endpoint."""
def setUp(self):
super(TestProgramsIdTokenView, self).setUp()
self.user = UserFactory()
self.client.login(username=self.user.username, password='test')
self.path = reverse('programs_id_token')
def test_config_disabled(self):
"""Ensure the endpoint returns 404 when Programs authoring is disabled."""
self.create_config(enable_studio_tab=False)
response = self.client.get(self.path)
self.assertEqual(response.status_code, 404)
def test_not_logged_in(self):
"""Ensure the endpoint denies access to unauthenticated users."""
self.create_config()
self.client.logout()
response = self.client.get(self.path)
self.assertEqual(response.status_code, 302)
self.assertIn(settings.LOGIN_URL, response['Location'])
@mock.patch('cms.djangoapps.contentstore.views.program.get_id_token', return_value='test-id-token')
def test_config_enabled(self, mock_get_id_token):
"""
Ensure the endpoint responds with a valid JSON payload when authoring
is enabled.
"""
self.create_config()
response = self.client.get(self.path)
self.assertEqual(response.status_code, 200)
payload = json.loads(response.content)
self.assertEqual(payload, {"id_token": "test-id-token"})
# this comparison is a little long-handed because we need to compare user instances directly
user, client_name = mock_get_id_token.call_args[0]
self.assertEqual(user, self.user)
self.assertEqual(client_name, "programs")

View File

@@ -26,7 +26,6 @@ class CourseMetadata(object):
'enrollment_end',
'tabs',
'graceperiod',
'checklists',
'show_timezone',
'format',
'graded',

View File

@@ -0,0 +1,28 @@
"""
CourseSettingsEncoder
"""
import datetime
import json
from json.encoder import JSONEncoder
from opaque_keys.edx.locations import Location
from openedx.core.djangoapps.models.course_details import CourseDetails
from xmodule.fields import Date
from .course_grading import CourseGradingModel
class CourseSettingsEncoder(json.JSONEncoder):
"""
Serialize CourseDetails, CourseGradingModel, datetime, and old
Locations
"""
def default(self, obj): # pylint: disable=method-hidden
if isinstance(obj, (CourseDetails, CourseGradingModel)):
return obj.__dict__
elif isinstance(obj, Location):
return obj.dict()
elif isinstance(obj, datetime.datetime):
return Date().to_json(obj)
else:
return JSONEncoder.default(self, obj)

View File

@@ -81,14 +81,6 @@
}
}
},
"OPEN_ENDED_GRADING_INTERFACE": {
"grading_controller": "grading_controller",
"password": "password",
"peer_grading": "peer_grading",
"staff_grading": "staff_grading",
"url": "http://localhost:18060/",
"username": "lms"
},
"DJFS": {
"type": "s3fs",
"bucket": "test",

View File

@@ -104,5 +104,9 @@
"THEME_NAME": "",
"TIME_ZONE": "America/New_York",
"WIKI_ENABLED": true,
"OAUTH_OIDC_ISSUER": "https://www.example.com/oauth2"
"OAUTH_OIDC_ISSUER": "https://www.example.com/oauth2",
"DEPRECATED_BLOCK_TYPES": [
"poll",
"survey"
]
}

View File

@@ -96,6 +96,9 @@ FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings
# Enable partner support link in Studio footer
FEATURES['PARTNER_SUPPORT_EMAIL'] = 'partner-support@example.com'
# Disable some block types to test block deprecation logic
DEPRECATED_BLOCK_TYPES = ['poll', 'survey']
########################### Entrance Exams #################################
FEATURES['ENTRANCE_EXAMS'] = True
@@ -109,6 +112,8 @@ YOUTUBE['TEXT_API']['url'] = "127.0.0.1:{0}/test_transcripts_youtube/".format(YO
FEATURES['ENABLE_COURSEWARE_INDEX'] = True
FEATURES['ENABLE_LIBRARY_INDEX'] = True
FEATURES['ORGANIZATIONS_APP'] = True
SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine"
# Path at which to store the mock index
MOCK_SEARCH_BACKING_FILE = (

View File

@@ -180,6 +180,8 @@ FEATURES = {
# Special Exams, aka Timed and Proctored Exams
'ENABLE_SPECIAL_EXAMS': False,
'ORGANIZATIONS_APP': False,
}
ENABLE_JASMINE = False
@@ -924,6 +926,9 @@ OPTIONAL_APPS = (
# milestones
'milestones',
# Organizations App (http://github.com/edx/edx-organizations)
'organizations',
)
@@ -1001,6 +1006,8 @@ ADVANCED_COMPONENT_TYPES = [
'pb-dashboard',
'poll',
'survey',
'activetable',
'vectordraw',
# Some of the XBlocks from pmitros repos are sometimes prototypes.
# Use with caution.
'concept', # Concept mapper. See https://github.com/pmitros/ConceptXBlock
@@ -1011,8 +1018,6 @@ ADVANCED_COMPONENT_TYPES = [
'rate', # Allows up-down voting of course content. See https://github.com/pmitros/RateXBlock
'split_test',
'combinedopenended',
'peergrading',
'notes',
'schoolyourself_review',
'schoolyourself_lesson',

View File

@@ -173,9 +173,6 @@ CACHES = {
},
}
# Add apps to Installed apps for testing
INSTALLED_APPS += ('openedx.core.djangoapps.call_stack_manager',)
# hide ratelimit warnings while running tests
filterwarnings('ignore', message='No request passed to the backend, unable to rate-limit')

View File

@@ -34,7 +34,6 @@
modules: getModulesList([
'js/factories/asset_index',
'js/factories/base',
'js/factories/checklists',
'js/factories/container',
'js/factories/course',
'js/factories/course_create_rerun',

View File

@@ -1,23 +0,0 @@
define(["backbone", "underscore", "js/models/checklist"],
function(Backbone, _, ChecklistModel) {
var ChecklistCollection = Backbone.Collection.extend({
model : ChecklistModel,
parse: function(response) {
_.each(response,
function( element, idx ) {
element.id = idx;
});
return response;
},
// Disable caching so the browser back button will work (checklists have links to other
// places within Studio).
fetch: function (options) {
options.cache = false;
return Backbone.Collection.prototype.fetch.call(this, options);
}
});
return ChecklistCollection;
});

View File

@@ -1,16 +0,0 @@
define([
'jquery', 'js/collections/checklist', 'js/views/checklist'
], function($, ChecklistCollection, ChecklistView) {
'use strict';
return function (handlerUrl) {
var checklistCollection = new ChecklistCollection(),
editor;
checklistCollection.url = handlerUrl;
editor = new ChecklistView({
el: $('.course-checklists'),
collection: checklistCollection
});
checklistCollection.fetch({reset: true});
};
});

View File

@@ -91,7 +91,7 @@ define(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape", "js/vie
$('.new-course-save').on('click', saveNewCourse);
$cancelButton.bind('click', makeCancelHandler('course'));
CancelOnEscape($cancelButton);
CreateCourseUtils.setupOrgAutocomplete();
CreateCourseUtils.configureHandlers();
};

View File

@@ -41,6 +41,8 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers
var requests = AjaxHelpers.requests(this);
var redirectSpy = spyOn(ViewUtils, 'redirect');
$('.new-course-button').click()
AjaxHelpers.expectJsonRequest(requests, 'GET', '/organizations');
AjaxHelpers.respondWithJson(requests, ['DemoX', 'DemoX2', 'DemoX3']);
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
$('.new-course-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/course/', {
@@ -53,11 +55,14 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers
url: 'dummy_test_url'
});
expect(redirectSpy).toHaveBeenCalledWith('dummy_test_url');
$(".new-course-org").autocomplete("destroy");
});
it("displays an error when saving fails", function () {
var requests = AjaxHelpers.requests(this);
$('.new-course-button').click();
AjaxHelpers.expectJsonRequest(requests, 'GET', '/organizations');
AjaxHelpers.respondWithJson(requests, ['DemoX', 'DemoX2', 'DemoX3']);
fillInFields('DemoX', 'DM101', '2014', 'Demo course');
$('.new-course-save').click();
AjaxHelpers.respondWithJson(requests, {
@@ -67,6 +72,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/spec_helpers
expect($('#course_creation_error')).toContainText('error message');
expect($('.new-course-save')).toHaveClass('is-disabled');
expect($('.new-course-save')).toHaveAttr('aria-disabled', 'true');
$(".new-course-org").autocomplete("destroy");
});
it("saves new libraries", function () {

View File

@@ -31,8 +31,7 @@ define([
entrance_exam_enabled : '',
entrance_exam_minimum_score_pct: '50',
license: null,
language: '',
has_cert_config: false
language: ''
},
mockSettingsPage = readFixtures('mock/mock-settings-page.underscore');

View File

@@ -1,93 +0,0 @@
define(["js/views/baseview", "underscore", "jquery"], function(BaseView, _, $) {
var ChecklistView = BaseView.extend({
// takes CMS.Models.Checklists as model
events : {
'click .course-checklist .checklist-title' : "toggleChecklist",
'click .course-checklist .task input' : "toggleTask",
'click a[rel="external"]' : "popup"
},
initialize : function() {
var self = this;
this.template = this.loadTemplate('checklist');
this.collection.fetch({
reset: true,
complete: function() {
self.render();
}
});
},
render: function() {
// catch potential outside call before template loaded
if (!this.template) return this;
this.$el.empty();
var self = this;
_.each(this.collection.models,
function(checklist, index) {
self.$el.append(self.renderTemplate(checklist, index));
});
return this;
},
renderTemplate: function (checklist, index) {
var checklistItems = checklist.attributes['items'];
var itemsChecked = 0;
_.each(checklistItems,
function(checklist) {
if (checklist['is_checked']) {
itemsChecked +=1;
}
});
var percentChecked = Math.round((itemsChecked/checklistItems.length)*100);
return this.template({
checklistIndex : index,
checklistShortDescription : checklist.attributes['short_description'],
items: checklistItems,
itemsChecked: itemsChecked,
percentChecked: percentChecked});
},
toggleChecklist : function(e) {
e.preventDefault();
$(e.target).closest('.course-checklist').toggleClass('is-collapsed');
},
toggleTask : function (e) {
var self = this;
var completed = 'is-completed';
var $checkbox = $(e.target);
var $task = $checkbox.closest('.task');
$task.toggleClass(completed);
var checklist_index = $checkbox.data('checklist');
var task_index = $checkbox.data('task');
var model = this.collection.at(checklist_index);
model.attributes.items[task_index].is_checked = $task.hasClass(completed);
model.save({},
{
success : function() {
var updatedTemplate = self.renderTemplate(model, checklist_index);
self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate);
analytics.track('Toggled a Checklist Task', {
'course': course_location_analytics,
'task': model.attributes.items[task_index].short_description,
'state': model.attributes.items[task_index].is_checked
});
}
});
},
popup: function(e) {
e.preventDefault();
window.open($(e.target).attr('href'));
}
});
return ChecklistView;
});

View File

@@ -194,7 +194,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
this.$('input.date').datepicker({'dateFormat': 'm/d/yy'});
this.$('input.time').timepicker({
'timeFormat' : 'H:i',
'forceRoundTime': true
'forceRoundTime': false
});
if (this.model.get(this.fieldName)) {
DateUtils.setDate(

View File

@@ -11,6 +11,14 @@ define(["jquery", "gettext", "common/js/components/utils/view_utils", "js/views/
CreateUtilsFactory.call(this, selectors, classes, keyLengthViolationMessage, keyFieldSelectors, nonEmptyCheckFieldSelectors);
this.setupOrgAutocomplete = function(){
$.getJSON('/organizations', function (data) {
$(selectors.org).autocomplete({
source: data
});
});
};
this.create = function (courseInfo, errorHandler) {
$.postJSON(
'/course/',

View File

@@ -61,7 +61,6 @@
@import 'views/unit';
@import 'views/container';
@import 'views/users';
@import 'views/checklists';
@import 'views/textbooks';
@import 'views/export-git';
@import 'views/group-configuration';

View File

@@ -368,12 +368,10 @@ body.course.view-certificates .nav-course-settings-certificates,
// course tools
body.course.view-import .nav-course-tools .title,
body.course.view-export .nav-course-tools .title,
body.course.view-checklists .nav-course-tools .title,
body.course.view-export-git .nav-course-tools .title,
body.course.view-import .nav-course-tools-import,
body.course.view-export .nav-course-tools-export,
body.course.view-checklists .nav-course-tools-checklists,
body.course.view-export-git .nav-course-tools-export-git,
// content library settings

View File

@@ -157,3 +157,35 @@
}
}
}
.ui-autocomplete {
position: absolute;
top: 0;
left: 0;
margin: 0;
padding: 0;
cursor: default;
@include linear-gradient($gray-l5, $white);
border-right: 1px solid $gray-l2;
border-bottom: 1px solid $gray-l2;
border-left: 1px solid $gray-l2;
background-color: $gray-l5;
box-shadow: inset 0 1px 2px $shadow-l1;
color: $color-copy-emphasized;
li.ui-menu-item{
padding: 0;
margin: 0;
a {
color: $color-copy-emphasized;
}
a.ui-state-focus{
border: none;
background-color: $blue;
background: $blue;
color: $white;
}
}
}

View File

@@ -1,341 +0,0 @@
// Studio - Course Settings
// ====================
.view-checklists {
.content-primary, .content-supplementary {
@include box-sizing(border-box);
}
.content-primary {
@extend .ui-col-wide;
}
// checklists - general
.course-checklist {
@extend %ui-window;
margin: 0 0 ($baseline*2) 0;
&:last-child {
margin-bottom: 0;
}
// visual status
.viz-checklist-status {
@extend %cont-text-hide;
@include size(100%,($baseline/4));
position: relative;
display: block;
margin: 0;
background: $gray-l4;
.viz-checklist-status-value {
@include transition(width $tmg-s2 ease-in-out .25s);
position: absolute;
top: 0;
left: 0;
width: 0%;
height: ($baseline/4);
background: $green;
.int {
@extend %cont-text-sr;
}
}
}
// header/title
header {
@include clearfix();
box-shadow: inset 0 -1px 1px $shadow-l1;
margin-bottom: 0;
border-bottom: 1px solid $gray-l3;
padding: $baseline ($baseline*1.5);
.checklist-title {
@include transition(color $tmg-f2 ease-in-out 0s);
width: flex-grid(6, 9);
margin: 0;
@include margin-right(flex-gutter());
@include float(left);
.ui-toggle-expansion {
@include transition(all $tmg-f2 ease-in-out 0s);
@extend %t-action1;
display: inline-block;
vertical-align: middle;
@include margin-right($baseline/2);
color: $gray-l4;
}
&.is-selectable {
@extend %ui-fake-link;
&:hover {
color: $blue;
.ui-toggle-expansion {
color: $blue;
}
}
}
}
.checklist-status {
@extend %t-copy-sub1;
width: flex-grid(3, 9);
@include float(right);
margin-top: ($baseline/2);
@include text-align(right);
color: $gray-l2;
.fa-check-square-o {
@extend %t-icon4;
display: inline-block;
margin-left: ($baseline/2);
color: $gray-l4;
}
.status-count {
@extend %t-copy-base;
@extend %t-strong;
margin-left: ($baseline/4);
margin-right: ($baseline/4);
color: $gray-d3;
}
.status-amount {
@extend %t-copy-base;
@extend %t-strong;
margin-left: ($baseline/4);
color: $gray-d3;
}
}
}
// checklist actions
.course-checklist-actions {
@include clearfix();
@include transition(border $tmg-f2 ease-in-out .25s);
box-shadow: inset 0 1px 1px $shadow-l1;
border-top: 1px solid $gray-l2;
padding: $baseline ($baseline*1.5);
background: $gray-l4;
.action-primary {
@include green-button();
@include float(left);
.fa-plus {
@extend %t-icon7;
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
.action-secondary {
@include grey-button();
@extend %t-action3;
@extend %t-regular;
@include float(right);
.fa-trash-o {
@extend %t-icon7;
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
}
// state - collapsed
&.is-collapsed {
header {
box-shadow: none;
.checklist-title {
.ui-toggle-expansion {
@include transform(rotate(-90deg));
@include transform-origin(50% 50%);
}
}
}
.list-tasks {
height: 0;
}
}
// state - completed
&.is-completed {
.viz-checklist-status {
.viz-checklist-status-value {
width: 100%;
}
}
header {
.checklist-title, .fa-caret-down {
color: $green;
}
.checklist-status {
.status-count, .status-amount, .fa-check-square-o {
color: $green;
}
}
}
}
// state - not available
.is-not-available {
}
}
// list of tasks
.list-tasks {
height: auto;
overflow: hidden;
.task {
@include transition(background $tmg-f2 ease-in-out 0s, border $tmg-f3 ease-in-out 0s);
@include clearfix();
position: relative;
border-top: 1px solid $white;
border-bottom: 1px solid $gray-l5;
padding: $baseline ($baseline*1.5);
background: $white;
opacity: 1.0;
&:last-child {
margin-bottom: 0;
border-bottom: none;
}
.task-input {
display: inline-block;
vertical-align: text-top;
@include float(left);
margin-top: ($baseline/2);
@include margin-right(flex-gutter());
}
.task-details {
@extend %t-strong;
display: inline-block;
vertical-align: text-top;
@include float(left);
width: flex-grid(6,9);
.task-name {
@include transition(color $tmg-f2 ease-in-out 0s);
@extend %ui-fake-link;
vertical-align: baseline;
margin-bottom: 0;
}
.task-description {
@extend %t-copy-sub1;
@include transition(color $tmg-f2 ease-in-out 0s);
color: $gray-l2;
}
.task-support {
@extend %t-copy-sub2;
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.0;
pointer-events: none;
}
}
.task-actions {
@include transition(opacity $tmg-f2 ease-in-out $tmg-f2);
@include clearfix();
display: inline-block;
vertical-align: middle;
@include float(right);
width: flex-grid(2,9);
margin: ($baseline/2) 0 0 flex-gutter();
opacity: 0.0;
pointer-events: none;
text-align: right;
.action-primary {
@extend %btn-primary-blue;
@extend %t-action3;
}
.action-secondary {
@extend %t-action4;
margin-top: ($baseline/2);
}
}
// state - hover
&:hover {
background: $blue-l5;
border-bottom-color: $blue-l4;
border-top-color: $blue-l4;
opacity: 1.0;
.task-details {
.task-support {
opacity: 1.0;
pointer-events: auto;
}
}
.task-actions {
opacity: 1.0;
pointer-events: auto;
}
}
// state - completed
&.is-completed {
background: $gray-l6;
border-top-color: $gray-l5;
border-bottom-color: $gray-l5;
.task-name {
color: $gray-l2;
}
.task-actions {
.action-primary {
@extend %btn-secondary-blue;
@extend %t-action3;
}
}
&:hover {
background: $gray-l5;
border-bottom-color: $gray-l4;
border-top-color: $gray-l4;
.task-details {
opacity:1.0;
}
}
}
}
}
.content-supplementary {
@extend .ui-col-narrow;
}
}

View File

@@ -1,71 +0,0 @@
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "checklist" %></%def>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
%>
<%block name="title">${_("Course Checklists")}</%block>
<%block name="bodyclass">is-signedin course view-checklists</%block>
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
% for template_name in ["checklist"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
</%block>
<%block name="requirejs">
require(["js/factories/checklists"], function (ChecklistsFactory) {
ChecklistsFactory("${handler_url}");
});
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">${_("Tools")}</small>
<span class="sr">&gt; </span>${_("Course Checklists")}
</h1>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<form id="course-checklists" class="course-checklists" method="post" action="">
<h2 class="title title-3 sr">${_("Current Checklists")}</h2>
</form>
</article>
<aside class="content-supplementary" role="complementary">
<div class="bit">
<h3 class="title title-3">${_("What are course checklists?")}</h3>
<p>
${_("Course checklists are tools to help you understand and keep track of all the steps necessary to get your course ready for students.")}
</p>
<p>
${_("Any changes you make to these checklists are saved automatically and are immediately visible to other course team members.")}
</p>
</div>
<div class="bit">
<h3 class="title title-3">${_("{studio_name} checklists").format(studio_name=settings.STUDIO_SHORT_NAME)}</h3>
<nav class="nav-page checklists-current">
<ol>
% for checklist in checklists:
<li class="nav-item">
<a rel="view" href="${'#course-checklist' + str(loop.index)}">${checklist['short_description']}</a>
</li>
% endfor
</ol>
</nav>
</div>
</aside>
</section>
</div>
</%block>

View File

@@ -79,7 +79,7 @@
## Translators: This is an example for the name of the organization sponsoring a course, seen when filling out the form to create a new course. The organization name cannot contain spaces.
## Translators: "e.g. UniversityX or OrganizationX" is a placeholder displayed when user put no data into this field.
<input class="new-course-org" id="new-course-org" type="text" name="new-course-org" required placeholder="${_('e.g. UniversityX or OrganizationX')}" aria-describedby="tip-new-course-org tip-error-new-course-org" />
<span class="tip" id="tip-new-course-org">${_("The name of the organization sponsoring the course.")} <strong>${_("Note: This is part of your course URL, so no spaces or special characters are allowed.")}</strong> ${_("This cannot be changed, but you can set a different display name in Advanced Settings later.")}</span>
<span class="tip" id="tip-new-course-org">${_("The name of the organization sponsoring the course.")} <strong>${_("Note: The organization name is part of the course URL")}</strong> ${_("This cannot be changed, but you can set a different display name in Advanced Settings later.")}</span>
<span class="tip tip-error is-hiding" id="tip-error-new-course-org"></span>
</li>

View File

@@ -1,71 +0,0 @@
<% var allChecked = itemsChecked == items.length; %>
<section
<% if (allChecked) { %>
class="course-checklist is-completed"
<% } else { %>
class="course-checklist"
<% } %>
id="<%= 'course-checklist' + checklistIndex %>">
<span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value" style="width: <%= percentChecked %>%;">
<%= _.template(
// Translators: The {pct_sign} here represents the percent sign, i.e., '%'
// in many languages. This is used to avoid Transifex's misinterpreting of
// '% o'. The percent sign is also translatable as a standalone string.
gettext("{number}{pct_sign} of checklists completed"),
// Translators: This is the percent sign. It will be used to represent a
// percent value out of 100, e.g. "58%" means "58/100".
{number: '<span class="int">' + percentChecked + '</span>', pct_sign: gettext('%')},
{interpolate: /\{(.+?)\}/g}
)
%>
</span></span>
<header>
<h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist">
<i class="icon fa fa-caret-down ui-toggle-expansion"></i>
<%= checklistShortDescription %></h3>
<span class="checklist-status status">
<%= gettext("Tasks Completed:") %> <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span>
<i class="icon fa fa-check-square-o"></i>
</span>
</header>
<ul class="list list-tasks">
<% var taskIndex = 0; %>
<% _.each(items, function(item) { %>
<% var checked = item['is_checked']; %>
<li
<% if (checked) { %>
class="task is-completed"
<% } else { %>
class="task"
<% } %>
>
<% var taskId = 'course-checklist' + checklistIndex + '-task' + taskIndex; %>
<input type="checkbox" class="task-input" data-checklist="<%= checklistIndex %>" data-task="<%= taskIndex %>"
name="<%= taskId %>" id="<%= taskId %>"
<% if (checked) { %>
checked="checked"
<% } %>
>
<label class="task-details" for="<%= taskId %>">
<h4 class="task-name title title-3"><%= item['short_description'] %></h4>
<p class="task-description"><%= item['long_description'] %></p>
</label>
<% if (item['action_text'] !== '' && item['action_url'] !== '') { %>
<ul class="list-actions task-actions">
<li class="action-item">
<a href="<%= item['action_url'] %>" class="action action-primary"
<% if (item['action_external']) { %>
rel="external" title="<%= gettext("This link will open in a new browser window/tab") %>"
<% } %>
><%= item['action_text'] %></a>
</li>
</ul>
<% } %>
</li>
<% taskIndex+=1; }) %>
</ul>
</section>

View File

@@ -12,5 +12,5 @@
</%block>
<%block name="content">
<div class="js-program-admin program-app layout-1q3q layout-reversed" data-api-url=${programs_api_url} data-home-url=${studio_home_url}></div>
<div class="js-program-admin program-app layout-1q3q layout-reversed" data-api-url=${programs_api_url} data-auth-url=${programs_token_url} data-home-url=${studio_home_url}></div>
</%block>

View File

@@ -8,8 +8,8 @@
import json
from contentstore import utils
from django.utils.translation import ugettext as _
from models.settings.encoder import CourseSettingsEncoder
from openedx.core.lib.js_utils import escape_json_dumps
from models.settings.course_details import CourseSettingsEncoder
%>
<%block name="header_extras">

View File

@@ -19,7 +19,6 @@
<%
course_key = context_course.id
index_url = reverse('contentstore.views.course_handler', kwargs={'course_key_string': unicode(course_key)})
checklists_url = reverse('contentstore.views.checklists_handler', kwargs={'course_key_string': unicode(course_key)})
course_team_url = reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(course_key)})
assets_url = reverse('contentstore.views.assets_handler', kwargs={'course_key_string': unicode(course_key)})
textbooks_url = reverse('contentstore.views.textbooks_list_handler', kwargs={'course_key_string': unicode(course_key)})
@@ -113,9 +112,6 @@
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
<li class="nav-item nav-course-tools-checklists">
<a href="${checklists_url}">${_("Checklists")}</a>
</li>
<li class="nav-item nav-course-tools-import">
<a href="${import_url}">${_("Import")}</a>
</li>

View File

@@ -1,97 +0,0 @@
<div class="wrapper-comp-editor" id="editor-tab">
<section class="combinedopenended-editor editor">
<div class="row">
%if enable_markdown:
<div class="editor-bar">
<ul class="format-buttons">
<li><a href="#" class="prompt-button" data-tooltip="Prompt"><span
class="combinedopenended-editor-icon fa fa-quote-left"></span></a></li>
<li><a href="#" class="rubric-button" data-tooltip="Rubric"><span
class="combinedopenended-editor-icon fa fa-table"></span></a></li>
<li><a href="#" class="tasks-button" data-tooltip="Tasks"><span
class="combinedopenended-editor-icon fa fa-sitemap"></span></a></li>
</ul>
<ul class="editor-tabs">
<li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">Advanced Editor</a></li>
<li><a href="#" class="cheatsheet-toggle" data-tooltip="Toggle Cheatsheet">?</a></li>
</ul>
</div>
<textarea class="markdown-box">${markdown | h}</textarea>
%endif
<textarea class="xml-box" rows="8" cols="40">${data | h}</textarea>
</div>
</section>
<script type="text/template" id="open-ended-template">
<openended %min_max_string%>
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "%grading_config%", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
</openendedparam>
</openended>
</script>
<script type="text/template" id="simple-editor-open-ended-cheatsheet">
<article class="simple-editor-open-ended-cheatsheet">
<div class="cheatsheet-wrapper">
<div class="row">
<h6>Prompt</h6>
<div class="col prompt">
</div>
<div class="col">
<pre><code>
[prompt]
Why is the sky blue?
[prompt]
</code></pre>
</div>
<div class="col">
<p>The student will respond to the prompt. The prompt can contain any html tags, such as paragraph tags and header tags.</p>
</div>
</div>
<div class="row">
<h6>Rubric</h6>
<div class="col sample rubric"><!DOCTYPE html>
</div>
<div class="col">
<pre><code>
[rubric]
+ Color Identification
- Incorrect
- Correct
+ Grammar
- Poor
- Acceptable
- Superb
[rubric]
</code></pre>
</div>
<div class="col">
<p>The rubric is used for feedback and self-assessment. The rubric can have as many categories (+) and options (-) as desired. </p>
<p>The total score for the problem will be the sum of all the points possible on the rubric. The options will be numbered sequentially from zero in each category, so each category will be worth as many points as its number of options minus one. </p>
</div>
</div>
<div class="row">
<h6>Tasks</h6>
<div class="col sample tasks">
</div>
<div class="col">
<pre><code>
[tasks]
(Self), ({1-3}AI), ({2-3}Peer)
[tasks]
</code></pre>
</div>
<div class="col">
<p>The tasks define what feedback the student will get from the problem.</p>
<p>Each task is defined with parentheses around it. Brackets (ie {2-3} above), specify the minimum and maximum score needed to attempt the given task.</p>
<p>In the example above, the student will first be asked to self-assess. If they give themselves greater than or equal to a 1/3 and less than or equal to a 3/3 on the problem, then they will be moved to AI assessment. If they score themselves a 2/3 or 3/3 on AI assessment, they will move to peer assessment.</p>
<p>Students will be given feedback from each task, and their final score for a given attempt of the problem will be their score last task that is completed.</p>
</div>
</div>
</div>
</article>
</script>
</div>
<%include file="metadata-edit.html" />

View File

@@ -3,8 +3,8 @@ from django.conf.urls import patterns, include, url
# There is a course creators admin table.
from ratelimitbackend import admin
from cms.djangoapps.contentstore.views.program import ProgramAuthoringView
from cms.djangoapps.contentstore.views.program import ProgramAuthoringView, ProgramsIdTokenView
from cms.djangoapps.contentstore.views.organization import OrganizationListView
admin.autodiscover()
@@ -41,6 +41,7 @@ urlpatterns = patterns(
url(r'^not_found$', 'contentstore.views.not_found', name='not_found'),
url(r'^server_error$', 'contentstore.views.server_error', name='server_error'),
url(r'^organizations$', OrganizationListView.as_view(), name='organizations'),
# temporary landing page for edge
url(r'^edge$', 'contentstore.views.edge', name='edge'),
@@ -92,7 +93,6 @@ urlpatterns += patterns(
'course_notifications_handler'),
url(r'^course_rerun/{}$'.format(settings.COURSE_KEY_PATTERN), 'course_rerun_handler', name='course_rerun_handler'),
url(r'^container/{}$'.format(settings.USAGE_KEY_PATTERN), 'container_handler'),
url(r'^checklists/{}/(?P<checklist_index>\d+)?$'.format(settings.COURSE_KEY_PATTERN), 'checklists_handler'),
url(r'^orphan/{}$'.format(settings.COURSE_KEY_PATTERN), 'orphan_handler'),
url(r'^assets/{}/{}?$'.format(settings.COURSE_KEY_PATTERN, settings.ASSET_KEY_PATTERN), 'assets_handler'),
url(r'^import/{}$'.format(COURSELIKE_KEY_PATTERN), 'import_handler'),
@@ -185,9 +185,10 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'):
)
urlpatterns += (
# These views use a configuration model to determine whether or not to
# display the Programs authoring app. If disabled, a 404 is returned.
url(r'^programs/id_token/$', ProgramsIdTokenView.as_view(), name='programs_id_token'),
# Drops into the Programs authoring app, which handles its own routing.
# The view uses a configuration model to determine whether or not to
# display the authoring app. If disabled, a 404 is returned.
url(r'^program/', ProgramAuthoringView.as_view(), name='programs'),
)

View File

@@ -14,7 +14,7 @@ from opaque_keys import InvalidKeyError
from util.date_utils import get_time_display
from xmodule.modulestore.django import modulestore
from course_modes.models import CourseMode
from course_modes.models import CourseMode, CourseModeExpirationConfig
# Technically, we shouldn't be doing this, since verify_student is defined
# in LMS, and course_modes is defined in common.
@@ -66,12 +66,13 @@ class CourseModeForm(forms.ModelForm):
default_tz = timezone(settings.TIME_ZONE)
if self.instance.expiration_datetime:
if self.instance._expiration_datetime: # pylint: disable=protected-access
# django admin is using default timezone. To avoid time conversion from db to form
# convert the UTC object to naive and then localize with default timezone.
expiration_datetime = self.instance.expiration_datetime.replace(tzinfo=None)
self.initial["expiration_datetime"] = default_tz.localize(expiration_datetime)
_expiration_datetime = self.instance._expiration_datetime.replace( # pylint: disable=protected-access
tzinfo=None
)
self.initial["_expiration_datetime"] = default_tz.localize(_expiration_datetime)
# Load the verification deadline
# Since this is stored on a model in verify student, we need to load it from there.
# We need to munge the timezone a bit to get Django admin to display it without converting
@@ -99,14 +100,14 @@ class CourseModeForm(forms.ModelForm):
return course_key
def clean_expiration_datetime(self):
def clean__expiration_datetime(self):
"""
Ensure that the expiration datetime we save uses the UTC timezone.
"""
# django admin saving the date with default timezone to avoid time conversion from form to db
# changes its tzinfo to UTC
if self.cleaned_data.get("expiration_datetime"):
return self.cleaned_data.get("expiration_datetime").replace(tzinfo=UTC)
if self.cleaned_data.get("_expiration_datetime"):
return self.cleaned_data.get("_expiration_datetime").replace(tzinfo=UTC)
def clean_verification_deadline(self):
"""
@@ -122,7 +123,7 @@ class CourseModeForm(forms.ModelForm):
"""
cleaned_data = super(CourseModeForm, self).clean()
mode_slug = cleaned_data.get("mode_slug")
upgrade_deadline = cleaned_data.get("expiration_datetime")
upgrade_deadline = cleaned_data.get("_expiration_datetime")
verification_deadline = cleaned_data.get("verification_deadline")
# Allow upgrade deadlines ONLY for the "verified" mode
@@ -181,7 +182,7 @@ class CourseModeAdmin(admin.ModelAdmin):
'mode_display_name',
'min_price',
'currency',
'expiration_datetime',
'_expiration_datetime',
'verification_deadline',
'sku'
)
@@ -206,4 +207,12 @@ class CourseModeAdmin(admin.ModelAdmin):
# in the Django admin list view.
expiration_datetime_custom.short_description = "Upgrade Deadline"
class CourseModeExpirationConfigAdmin(admin.ModelAdmin):
"""Admin interface for the course mode auto expiration configuration. """
class Meta(object):
model = CourseModeExpirationConfig
admin.site.register(CourseMode, CourseModeAdmin)
admin.site.register(CourseModeExpirationConfig, CourseModeExpirationConfigAdmin)

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_modes', '0002_coursemode_expiration_datetime_is_explicit'),
]
operations = [
migrations.AlterField(
model_name='coursemode',
name='expiration_datetime_is_explicit',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import timedelta
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('course_modes', '0003_auto_20151113_1443'),
]
operations = [
migrations.CreateModel(
name='CourseModeExpirationConfig',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('verification_window', models.DurationField(default=timedelta(10), help_text='The time period before a course ends in which a course mode will expire')),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
options={
'ordering': ('-change_date',),
'abstract': False,
},
),
]

View File

@@ -1,15 +1,15 @@
"""
Add and create new modes for running courses on this particular LMS
"""
from datetime import datetime, timedelta
import pytz
from datetime import datetime
from collections import namedtuple, defaultdict
from config_models.models import ConfigurationModel
from django.core.exceptions import ValidationError
from django.db import models
from collections import namedtuple, defaultdict
from django.utils.translation import ugettext_lazy as _
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from xmodule_django.models import CourseKeyField
Mode = namedtuple('Mode',
@@ -54,19 +54,20 @@ class CourseMode(models.Model):
# For example, if there is a verified mode that expires on 1/1/2015,
# then users will be able to upgrade into the verified mode before that date.
# Once the date passes, users will no longer be able to enroll as verified.
expiration_datetime = models.DateTimeField(
_expiration_datetime = models.DateTimeField(
default=None, null=True, blank=True,
verbose_name=_(u"Upgrade Deadline"),
help_text=_(
u"OPTIONAL: After this date/time, users will no longer be able to enroll in this mode. "
u"Leave this blank if users can enroll in this mode until enrollment closes for the course."
),
db_column='expiration_datetime',
)
# The system prefers to set this automatically based on default settings. But
# if the field is set manually we want a way to indicate that so we don't
# overwrite the manual setting of the field.
expiration_datetime_is_explicit = models.BooleanField(default=True)
expiration_datetime_is_explicit = models.BooleanField(default=False)
# DEPRECATED: the `expiration_date` field has been replaced by `expiration_datetime`
expiration_date = models.DateField(default=None, null=True, blank=True)
@@ -150,6 +151,17 @@ class CourseMode(models.Model):
"""
return self.mode_slug
@property
def expiration_datetime(self):
""" Return _expiration_datetime. """
return self._expiration_datetime
@expiration_datetime.setter
def expiration_datetime(self, new_datetime):
""" Saves datetime to _expiration_datetime and sets the explicit flag. """
self.expiration_datetime_is_explicit = True
self._expiration_datetime = new_datetime
@classmethod
def all_modes_for_courses(cls, course_id_list):
"""Find all modes for a list of course IDs, including expired modes.
@@ -223,8 +235,8 @@ class CourseMode(models.Model):
Q(course_id=course_id) &
Q(min_price__gt=0) &
(
Q(expiration_datetime__isnull=True) |
Q(expiration_datetime__gte=now)
Q(_expiration_datetime__isnull=True) |
Q(_expiration_datetime__gte=now)
)
)
return [mode.to_tuple() for mode in found_course_modes]
@@ -259,7 +271,7 @@ class CourseMode(models.Model):
# Filter out expired course modes if include_expired is not set
if not include_expired:
found_course_modes = found_course_modes.filter(
Q(expiration_datetime__isnull=True) | Q(expiration_datetime__gte=now)
Q(_expiration_datetime__isnull=True) | Q(_expiration_datetime__gte=now)
)
# Credit course modes are currently not shown on the track selection page;
@@ -633,3 +645,19 @@ class CourseModesArchive(models.Model):
expiration_date = models.DateField(default=None, null=True, blank=True)
expiration_datetime = models.DateTimeField(default=None, null=True, blank=True)
class CourseModeExpirationConfig(ConfigurationModel):
"""
Configuration for time period from end of course to auto-expire a course mode.
"""
verification_window = models.DurationField(
default=timedelta(days=10),
help_text=_(
"The time period before a course ends in which a course mode will expire"
)
)
def __unicode__(self):
""" Returns the unicode date of the verification window. """
return unicode(self.verification_window)

View File

@@ -0,0 +1,36 @@
"""
Signal handler for setting default course mode expiration dates
"""
from django.core.exceptions import ObjectDoesNotExist
from django.dispatch.dispatcher import receiver
from xmodule.modulestore.django import SignalHandler, modulestore
from .models import CourseMode, CourseModeExpirationConfig
@receiver(SignalHandler.course_published)
def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument
"""
Catches the signal that a course has been published in Studio and
sets the verified mode dates to defaults.
"""
try:
verified_mode = CourseMode.objects.get(course_id=course_key, mode_slug=CourseMode.VERIFIED)
if _should_update_date(verified_mode):
course = modulestore().get_course(course_key)
if not course:
return None
verification_window = CourseModeExpirationConfig.current().verification_window
new_expiration_datetime = course.end - verification_window
if verified_mode.expiration_datetime != new_expiration_datetime:
# Set the expiration_datetime without triggering the explicit flag
verified_mode._expiration_datetime = new_expiration_datetime # pylint: disable=protected-access
verified_mode.save()
except ObjectDoesNotExist:
pass
def _should_update_date(verified_mode):
""" Returns whether or not the verified mode should be updated. """
return not(verified_mode is None or verified_mode.expiration_datetime_is_explicit)

View File

@@ -0,0 +1,4 @@
"""
Setup the signals on startup.
"""
import course_modes.signals # pylint: disable=unused-import

View File

@@ -48,8 +48,8 @@ class AdminCourseModePageTest(ModuleStoreTestCase):
'mode_display_name': 'verified',
'min_price': 10,
'currency': 'usd',
'expiration_datetime_0': expiration.date(), # due to django admin datetime widget passing as seperate vals
'expiration_datetime_1': expiration.time(),
'_expiration_datetime_0': expiration.date(), # due to django admin datetime widget passing as separate vals
'_expiration_datetime_1': expiration.time(),
}
@@ -201,7 +201,7 @@ class AdminCourseModeFormTest(ModuleStoreTestCase):
"course_id": unicode(self.course.id),
"mode_slug": mode,
"mode_display_name": mode,
"expiration_datetime": upgrade_deadline,
"_expiration_datetime": upgrade_deadline,
"currency": "usd",
"min_price": 10,
}, instance=course_mode)

View File

@@ -49,7 +49,7 @@ class CourseModeModelTest(TestCase):
min_price=min_price,
suggested_prices=suggested_prices,
currency=currency,
expiration_datetime=expiration_datetime,
_expiration_datetime=expiration_datetime,
)
def test_save(self):
@@ -403,3 +403,21 @@ class CourseModeModelTest(TestCase):
return dict(zip(dict_keys, display_values.get('verify_none')))
else:
return dict(zip(dict_keys, display_values.get(dict_type)))
def test_expiration_datetime_explicitly_set(self):
""" Verify that setting the expiration_date property sets the explicit flag. """
verified_mode, __ = self.create_mode('verified', 'Verified Certificate')
now = datetime.now()
verified_mode.expiration_datetime = now
self.assertTrue(verified_mode.expiration_datetime_is_explicit)
self.assertEqual(verified_mode.expiration_datetime, now)
def test_expiration_datetime_not_explicitly_set(self):
""" Verify that setting the _expiration_date property does not set the explicit flag. """
verified_mode, __ = self.create_mode('verified', 'Verified Certificate')
now = datetime.now()
verified_mode._expiration_datetime = now # pylint: disable=protected-access
self.assertFalse(verified_mode.expiration_datetime_is_explicit)
self.assertEqual(verified_mode.expiration_datetime, now)

View File

@@ -0,0 +1,89 @@
"""
Unit tests for the course_mode signals
"""
from datetime import datetime, timedelta
from mock import patch
import ddt
from pytz import UTC
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from course_modes.models import CourseMode
from course_modes.signals import _listen_for_course_publish
@ddt.ddt
class CourseModeSignalTest(ModuleStoreTestCase):
"""
Tests for the course_mode course_published signal.
"""
def setUp(self):
super(CourseModeSignalTest, self).setUp()
self.end = datetime.now(tz=UTC).replace(microsecond=0) + timedelta(days=7)
self.course = CourseFactory.create(end=self.end)
CourseMode.objects.all().delete()
def create_mode(
self,
mode_slug,
mode_name,
min_price=0,
suggested_prices='',
currency='usd',
expiration_datetime=None,
):
"""
Create a new course mode
"""
return CourseMode.objects.get_or_create(
course_id=self.course.id,
mode_display_name=mode_name,
mode_slug=mode_slug,
min_price=min_price,
suggested_prices=suggested_prices,
currency=currency,
_expiration_datetime=expiration_datetime,
)
def test_no_verified_mode(self):
""" Verify expiration not updated by signal for non-verified mode. """
course_mode, __ = self.create_mode('honor', 'honor')
_listen_for_course_publish('store', self.course.id)
course_mode.refresh_from_db()
self.assertIsNone(course_mode.expiration_datetime)
@ddt.data(1, 14, 30)
def test_verified_mode(self, verification_window):
""" Verify signal updates expiration to configured time period before course end for verified mode. """
course_mode, __ = self.create_mode('verified', 'verified')
self.assertIsNone(course_mode.expiration_datetime)
with patch('course_modes.models.CourseModeExpirationConfig.current') as config:
instance = config.return_value
instance.verification_window = timedelta(days=verification_window)
_listen_for_course_publish('store', self.course.id)
course_mode.refresh_from_db()
self.assertEqual(course_mode.expiration_datetime, self.end - timedelta(days=verification_window))
@ddt.data(1, 14, 30)
def test_verified_mode_explicitly_set(self, verification_window):
""" Verify signal does not update expiration for verified mode with explicitly set expiration. """
course_mode, __ = self.create_mode('verified', 'verified')
course_mode.expiration_datetime_is_explicit = True
self.assertIsNone(course_mode.expiration_datetime)
with patch('course_modes.models.CourseModeExpirationConfig.current') as config:
instance = config.return_value
instance.verification_window = timedelta(days=verification_window)
_listen_for_course_publish('store', self.course.id)
course_mode.refresh_from_db()
self.assertEqual(course_mode.expiration_datetime, self.end - timedelta(days=verification_window))

View File

@@ -150,10 +150,9 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
throttle = EnrollmentUserThrottle()
self.rate_limit, rate_duration = throttle.parse_rate(throttle.rate)
self.course = CourseFactory.create()
# Load a CourseOverview. This initial load should result in a cache
# miss; the modulestore is queried and course metadata is cached.
__ = CourseOverview.get_from_id(self.course.id)
# Pass emit_signals when creating the course so it would be cached
# as a CourseOverview.
self.course = CourseFactory.create(emit_signals=True)
self.user = UserFactory.create(
username=self.USERNAME,
@@ -336,7 +335,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
requesting user.
"""
# Create another course, and enroll self.user in both courses.
other_course = CourseFactory.create()
other_course = CourseFactory.create(emit_signals=True)
for course in self.course, other_course:
CourseModeFactory.create(
course_id=unicode(course.id),
@@ -345,7 +344,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
)
self.assert_enrollment_status(
course_id=unicode(course.id),
max_mongo_calls=1,
max_mongo_calls=0,
)
# Verify the user himself can see both of his enrollments.
self._assert_enrollments_visible_in_list([self.course, other_course])

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('student', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='courseenrollment',
name='mode',
field=models.CharField(default=b'audit', max_length=100),
),
migrations.AlterField(
model_name='historicalcourseenrollment',
name='mode',
field=models.CharField(default=b'audit', max_length=100),
),
]

View File

@@ -1,6 +1,7 @@
from django.test import TestCase
from django.test.client import Client
from django.contrib.auth.models import User
from django.conf import settings
from django_comment_common.models import (
Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_STUDENT)
from django_comment_common.utils import seed_permissions_roles
@@ -175,7 +176,47 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
response_data
)
def _auto_auth(self, params=None, **kwargs):
@ddt.data(*COURSE_IDS_DDT)
@ddt.unpack
def test_redirect_to_course(self, course_id, course_key):
# Create a user and enroll in a course
response = self._auto_auth({
'username': 'test',
'course_id': course_id,
'redirect': True,
'staff': 'true',
}, status_code=302)
# Check that a course enrollment was created for the user
self.assertEqual(CourseEnrollment.objects.count(), 1)
enrollment = CourseEnrollment.objects.get(course_id=course_key)
self.assertEqual(enrollment.user.username, "test")
# Check that the redirect was to the course info/outline page
if settings.ROOT_URLCONF == 'lms.urls':
url_pattern = '/info'
else:
url_pattern = '/course/{}'.format(unicode(course_key))
self.assertTrue(response.url.endswith(url_pattern)) # pylint: disable=no-member
def test_redirect_to_main(self):
# Create user and redirect to 'home' (cms) or 'dashboard' (lms)
response = self._auto_auth({
'username': 'test',
'redirect': True,
'staff': 'true',
}, status_code=302)
# Check that the redirect was to either /dashboard or /home
if settings.ROOT_URLCONF == 'lms.urls':
url_pattern = '/dashboard'
else:
url_pattern = '/home'
self.assertTrue(response.url.endswith(url_pattern)) # pylint: disable=no-member
def _auto_auth(self, params=None, status_code=None, **kwargs):
"""
Make a request to the auto-auth end-point and check
that the response is successful.
@@ -189,7 +230,9 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
"""
params = params or {}
response = self.client.get(self.url, params, **kwargs)
self.assertEqual(response.status_code, 200)
expected_status_code = status_code if status_code else 200
self.assertEqual(response.status_code, expected_status_code)
# Check that session and CSRF are set in the response
for cookie in ['csrftoken', 'sessionid']:

View File

@@ -82,12 +82,6 @@ class CertificateDisplayTest(ModuleStoreTestCase):
@override_settings(CERT_NAME_SHORT='Test_Certificate')
@patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True})
def test_linked_student_to_web_view_credential(self, enrollment_mode):
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course.id)
)
self._create_certificate(enrollment_mode)
certificates = [
{
'id': 0,
@@ -103,6 +97,9 @@ class CertificateDisplayTest(ModuleStoreTestCase):
self.course.save() # pylint: disable=no-member
self.store.update_item(self.course, self.user.id)
cert = self._create_certificate(enrollment_mode)
test_url = get_certificate_url(course_id=self.course.id, uuid=cert.verify_uuid)
response = self.client.get(reverse('dashboard'))
self.assertContains(response, u'View Test_Certificate')
@@ -165,7 +162,7 @@ class CertificateDisplayTest(ModuleStoreTestCase):
def _create_certificate(self, enrollment_mode):
"""Simulate that the user has a generated certificate. """
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, mode=enrollment_mode)
GeneratedCertificateFactory(
return GeneratedCertificateFactory(
user=self.user,
course_id=self.course.id,
mode=enrollment_mode,

View File

@@ -116,9 +116,8 @@ class CreditCourseDashboardTest(ModuleStoreTestCase):
self._make_eligible()
self._purchase_credit()
# Expect that the user's status is "pending"
response = self._load_dashboard()
self.assertContains(response, "credit-request-pending-msg")
self.assertContains(response, "credit-request-not-started-msg")
def test_purchased_credit_and_request_pending(self):
# Simulate that the user has purchased credit and initiated a request,

View File

@@ -21,7 +21,7 @@ from django.contrib.auth.views import password_reset_confirm
from django.contrib import messages
from django.core.context_processors import csrf
from django.core import mail
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse, NoReverseMatch
from django.core.validators import validate_email, ValidationError
from django.db import IntegrityError, transaction
from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseForbidden,
@@ -338,13 +338,9 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
# showing the certificate web view button if certificate is ready state and feature flags are enabled.
if has_html_certificates_enabled(course_overview.id, course_overview):
if course_overview.has_any_active_web_certificate:
certificate_url = get_certificate_url(
user_id=user.id,
course_id=unicode(course_overview.id),
)
status_dict.update({
'show_cert_web_view': True,
'cert_web_view_url': u'{url}'.format(url=certificate_url)
'cert_web_view_url': get_certificate_url(course_id=course_overview.id, uuid=cert_status['uuid'])
})
else:
# don't show download certificate button if we don't have an active certificate for course
@@ -1802,6 +1798,7 @@ def auto_auth(request):
* `course_id`: Enroll the student in the course with `course_id`
* `roles`: Comma-separated list of roles to grant the student in the course with `course_id`
* `no_login`: Define this to create the user but not login
* `redirect`: Set to "true" will redirect to course if course_id is defined, otherwise it will redirect to dashboard
If username, email, or password are not provided, use
randomly generated credentials.
@@ -1826,6 +1823,7 @@ def auto_auth(request):
if course_id:
course_key = CourseLocator.from_string(course_id)
role_names = [v.strip() for v in request.GET.get('roles', '').split(',') if v.strip()]
redirect_when_done = request.GET.get('redirect', '').lower() == 'true'
login_when_done = 'no_login' not in request.GET
form = AccountCreationForm(
@@ -1888,8 +1886,32 @@ def auto_auth(request):
create_comments_service_user(user)
# Provide the user with a valid CSRF token
# then return a 200 response
if request.META.get('HTTP_ACCEPT') == 'application/json':
# then return a 200 response unless redirect is true
if redirect_when_done:
# Redirect to course info page if course_id is known
if course_id:
try:
# redirect to course info page in LMS
redirect_url = reverse(
'info',
kwargs={'course_id': course_id}
)
except NoReverseMatch:
# redirect to course outline page in Studio
redirect_url = reverse(
'course_handler',
kwargs={'course_key_string': course_id}
)
else:
try:
# redirect to dashboard for LMS
redirect_url = reverse('dashboard')
except NoReverseMatch:
# redirect to home for Studio
redirect_url = reverse('home')
return redirect(redirect_url)
elif request.META.get('HTTP_ACCEPT') == 'application/json':
response = JsonResponse({
'created_status': u"Logged in" if login_when_done else "Created",
'username': username,

View File

@@ -1,535 +0,0 @@
"""
Stub implementation of ORA service.
This is an extremely simple version of the service, with most
business logic removed. In particular, the stub:
1) Provides an infinite number of peer and calibration essays,
with dummy data.
2) Simulates a set number of pending submissions for each student;
grades submitted by one student are not used for any other student.
3) Ignores the scores/feedback students submit.
4) Ignores problem location: an essay graded for *any* problem is graded
for *every* problem.
Basically, the stub tracks only the *number* of peer/calibration essays
submitted by each student.
"""
import json
import pkg_resources
from .http import StubHttpRequestHandler, StubHttpService, require_params
class StudentState(object):
"""
Store state about the student that the stub
ORA implementation needs to keep track of.
"""
INITIAL_ESSAYS_AVAILABLE = 3
NUM_ESSAYS_REQUIRED = 1
NUM_CALIBRATION_REQUIRED = 1
def __init__(self):
self.num_graded = 0
self.num_calibrated = 0
def grade_peer_essay(self):
self.num_graded += 1
def grade_calibration_essay(self):
self.num_calibrated += 1
@property
def num_pending(self):
return max(self.INITIAL_ESSAYS_AVAILABLE - self.num_graded, 0)
@property
def num_required(self):
return max(self.NUM_ESSAYS_REQUIRED - self.num_graded, 0)
@property
def is_calibrated(self):
return self.num_calibrated >= self.NUM_CALIBRATION_REQUIRED
class StubOraHandler(StubHttpRequestHandler):
"""
Handler for ORA requests.
"""
GET_URL_HANDLERS = {
'/peer_grading/get_next_submission': '_get_next_submission',
'/peer_grading/is_student_calibrated': '_is_student_calibrated',
'/peer_grading/show_calibration_essay': '_show_calibration_essay',
'/peer_grading/get_notifications': '_get_notifications',
'/peer_grading/get_data_for_location': '_get_data_for_location',
'/peer_grading/get_problem_list': '_get_problem_list',
}
POST_URL_HANDLERS = {
'/peer_grading/save_grade': '_save_grade',
'/peer_grading/save_calibration_essay': '_save_calibration_essay',
# Test-specific, used by the XQueue stub to register a new submission,
# which we use to discover valid problem locations in the LMS
'/test/register_submission': '_register_submission'
}
def do_GET(self):
"""
Handle GET methods to the ORA API stub.
"""
self._send_handler_response('GET')
def do_POST(self):
"""
Handle POST methods to the ORA API stub.
"""
self._send_handler_response('POST')
def _send_handler_response(self, method):
"""
Delegate response to handler methods.
If no handler defined, send a 404 response.
"""
# Choose the list of handlers based on the HTTP method
if method == 'GET':
handler_list = self.GET_URL_HANDLERS
elif method == 'POST':
handler_list = self.POST_URL_HANDLERS
else:
self.log_error('Unrecognized method "{method}"'.format(method=method))
return
# Check the path (without querystring params) against our list of handlers
handler_name = handler_list.get(self.path_only)
if handler_name is not None:
handler = getattr(self, handler_name, None)
else:
handler = None
# Delegate to the handler to send a response
if handler is not None:
handler()
# If we don't have a handler for this URL and/or HTTP method,
# respond with a 404. This is the same behavior as the ORA API.
else:
self.send_response(404)
@require_params('GET', 'student_id', 'problem_id')
def _is_student_calibrated(self):
"""
Query whether the student has completed enough calibration
essays to begin peer grading.
Method: GET
Params:
- student_id
- problem_id
Result (JSON):
- success (bool)
- total_calibrated_on_so_far (int)
- calibrated (bool)
"""
student = self._student('GET')
if student is None:
self._error_response()
else:
self._success_response({
'total_calibrated_on_so_far': student.num_calibrated,
'calibrated': student.is_calibrated
})
@require_params('GET', 'student_id', 'problem_id')
def _show_calibration_essay(self):
"""
Retrieve a calibration essay for the student to grade.
Method: GET
Params:
- student_id
- problem_id
Result (JSON):
- success (bool)
- submission_id (str)
- submission_key (str)
- student_response (str)
- prompt (str)
- rubric (str)
- max_score (int)
"""
self._success_response({
'submission_id': self.server.DUMMY_DATA['submission_id'],
'submission_key': self.server.DUMMY_DATA['submission_key'],
'student_response': self.server.DUMMY_DATA['student_response'],
'prompt': self.server.DUMMY_DATA['prompt'],
'rubric': self.server.DUMMY_DATA['rubric'],
'max_score': self.server.DUMMY_DATA['max_score']
})
@require_params('GET', 'student_id', 'course_id')
def _get_notifications(self):
"""
Query counts of submitted, required, graded, and available peer essays
for a particular student.
Method: GET
Params:
- student_id
- course_id
Result (JSON):
- success (bool)
- student_sub_count (int)
- count_required (int)
- count_graded (int)
- count_available (int)
"""
student = self._student('GET')
if student is None:
self._error_response()
else:
self._success_response({
'student_sub_count': self.server.DUMMY_DATA['student_sub_count'],
'count_required': student.num_required,
'count_graded': student.num_graded,
'count_available': student.num_pending
})
@require_params('GET', 'student_id', 'location')
def _get_data_for_location(self):
"""
Query counts of submitted, required, graded, and available peer essays
for a problem location.
This will send an error response if the problem has not
been registered at the given `location`. This allows us
to ignore problems that are self- or ai-graded.
Method: GET
Params:
- student_id
- location
Result (JSON):
- success (bool)
- student_sub_count (int)
- count_required (int)
- count_graded (int)
- count_available (int)
"""
student = self._student('GET')
location = self.get_params.get('location')
# Do not return data if we're missing the student param
# or the problem has not yet been registered.
if student is None or location not in self.server.problems:
self._error_response()
else:
self._success_response({
'student_sub_count': self.server.DUMMY_DATA['student_sub_count'],
'count_required': student.num_required,
'count_graded': student.num_graded,
'count_available': student.num_pending
})
@require_params('GET', 'grader_id', 'location')
def _get_next_submission(self):
"""
Retrieve the next submission for the student to peer-grade.
Method: GET
Params:
- grader_id
- location
Result (JSON):
- success (bool)
- submission_id (str)
- submission_key (str)
- student_response (str)
- prompt (str, HTML)
- rubric (str, XML)
- max_score (int)
"""
self._success_response({
'submission_id': self.server.DUMMY_DATA['submission_id'],
'submission_key': self.server.DUMMY_DATA['submission_key'],
'student_response': self.server.DUMMY_DATA['student_response'],
'prompt': self.server.DUMMY_DATA['prompt'],
'rubric': self.server.DUMMY_DATA['rubric'],
'max_score': self.server.DUMMY_DATA['max_score']
})
@require_params('GET', 'course_id')
def _get_problem_list(self):
"""
Retrieve the list of problems available for peer grading.
Method: GET
Params:
- course_id
Result (JSON):
- success (bool)
- problem_list (list)
where `problem_list` is a list of dictionaries with keys:
- location (str)
- problem_name (str)
- num_graded (int)
- num_pending (int)
- num_required (int)
"""
self._success_response({'problem_list': self.server.problem_list})
@require_params('POST', 'grader_id', 'location', 'submission_id', 'score', 'feedback', 'submission_key')
def _save_grade(self):
"""
Save a score and feedback for an essay the student has graded.
Method: POST
Params:
- grader_id
- location
- submission_id
- score
- feedback
- submission_key
Result (JSON):
- success (bool)
"""
student = self._student('POST', key='grader_id')
if student is None:
self._error_response()
else:
# Update the number of essays the student has graded
student.grade_peer_essay()
return self._success_response({})
@require_params('POST', 'student_id', 'location', 'calibration_essay_id', 'score', 'feedback', 'submission_key')
def _save_calibration_essay(self):
"""
Save a score and feedback for a calibration essay the student has graded.
Returns the scores/feedback that the instructor gave for the essay.
Method: POST
Params:
- student_id
- location
- calibration_essay_id
- score
- feedback
- submission_key
Result (JSON):
- success (bool)
- message (str)
- actual_score (int)
- actual_rubric (str, XML)
- actual_feedback (str)
"""
student = self._student('POST')
if student is None:
self._error_response()
else:
# Increment the student calibration count
student.grade_calibration_essay()
self._success_response({
'message': self.server.DUMMY_DATA['message'],
'actual_score': self.server.DUMMY_DATA['actual_score'],
'actual_rubric': self.server.DUMMY_DATA['actual_rubric'],
'actual_feedback': self.server.DUMMY_DATA['actual_feedback']
})
@require_params('POST', 'grader_payload')
def _register_submission(self):
"""
Test-specific method to register a new submission.
This is used by `get_problem_list` to return valid locations in the LMS courseware.
In tests, this end-point gets called by the XQueue stub when it receives new submissions,
much like ORA discovers locations when students submit peer-graded problems to the XQueue.
Since the LMS sends *all* open-ended problems to the XQueue (including self- and ai-graded),
we have to ignore everything except peer-graded problems. We do so by looking
for the text 'peer' in the problem's name. This is a little bit of a hack,
but it makes the implementation much simpler.
Method: POST
Params:
- grader_payload (JSON dict)
Result: Empty
The only keys we use in `grader_payload` are 'location' and 'problem_id'.
"""
# Since this is a required param, we know it is in the post dict
try:
payload = json.loads(self.post_dict['grader_payload'])
except ValueError:
self.log_message(
"Could not decode grader payload as JSON: '{0}'".format(
self.post_dict['grader_payload']))
self.send_response(400)
else:
location = payload.get('location')
name = payload.get('problem_id')
if location is not None and name is not None:
if "peer" in name.lower():
self.server.register_problem(location, name)
self.send_response(200)
else:
self.log_message(
"Problem '{0}' does not have 'peer' in its name. Ignoring...".format(name)
)
self.send_response(200)
else:
self.log_message(
"Grader payload should contain 'location' and 'problem_id' keys: {0}".format(payload)
)
self.send_response(400)
def _student(self, method, key='student_id'):
"""
Return the `StudentState` instance for the student ID given
in the request parameters.
`method` is the HTTP request method (either "GET" or "POST")
and `key` is the parameter key.
"""
if method == 'GET':
student_id = self.get_params.get(key)
elif method == 'POST':
student_id = self.post_dict.get(key)
else:
self.log_error("Unrecognized method '{method}'".format(method=method))
return None
if student_id is None:
self.log_error("Could not get student ID from parameters")
return None
return self.server.student_state(student_id)
def _success_response(self, response_dict):
"""
Send a success response.
`response_dict` is a Python dictionary to JSON-encode.
"""
response_dict['success'] = True
response_dict['version'] = 1
self.send_response(
200, content=json.dumps(response_dict),
headers={'Content-type': 'application/json'}
)
def _error_response(self):
"""
Send an error response.
"""
response_dict = {'success': False, 'version': 1}
self.send_response(
400, content=json.dumps(response_dict),
headers={'Content-type': 'application/json'}
)
class StubOraService(StubHttpService):
"""
Stub ORA service.
"""
HANDLER_CLASS = StubOraHandler
DUMMY_DATA = {
'submission_id': 1,
'submission_key': 'test key',
'student_response': 'Test response',
'prompt': 'Test prompt',
'rubric': pkg_resources.resource_string(__name__, "data/ora_rubric.xml"),
'max_score': 2,
'message': 'Successfully saved calibration record.',
'actual_score': 2,
'actual_rubric': pkg_resources.resource_string(__name__, "data/ora_graded_rubric.xml"),
'actual_feedback': 'Great job!',
'student_sub_count': 1,
'problem_name': 'test problem',
'problem_list_num_graded': 1,
'problem_list_num_pending': 1,
'problem_list_num_required': 0,
}
def __init__(self, *args, **kwargs):
"""
Initialize student submission state.
"""
super(StubOraService, self).__init__(*args, **kwargs)
# Create a dict to map student ID's to their state
self._students = dict()
# By default, no problems are available for peer grading
# You can add to this list using the `register_location` HTTP end-point
# This is a dict mapping problem locations to problem names
self.problems = dict()
def student_state(self, student_id):
"""
Return the `StudentState` (named tuple) for the student
with ID `student_id`. The student state can be modified by the caller.
"""
# Create the student state if it does not already exist
if student_id not in self._students:
student = StudentState()
self._students[student_id] = student
# Retrieve the student state
return self._students[student_id]
@property
def problem_list(self):
"""
Return a list of problems available for peer grading.
"""
return [{
'location': location, 'problem_name': name,
'num_graded': self.DUMMY_DATA['problem_list_num_graded'],
'num_pending': self.DUMMY_DATA['problem_list_num_pending'],
'num_required': self.DUMMY_DATA['problem_list_num_required']
} for location, name in self.problems.items()]
def register_problem(self, location, name):
"""
Register a new problem with `location` and `name` for peer grading.
"""
self.problems[location] = name

View File

@@ -7,7 +7,6 @@ import logging
from .comments import StubCommentsService
from .xqueue import StubXQueueService
from .youtube import StubYouTubeService
from .ora import StubOraService
from .lti import StubLtiService
from .video_source import VideoSourceHttpService
from .edxnotes import StubEdxNotesService
@@ -19,7 +18,6 @@ USAGE = "USAGE: python -m stubs.start SERVICE_NAME PORT_NUM [CONFIG_KEY=CONFIG_V
SERVICES = {
'xqueue': StubXQueueService,
'youtube': StubYouTubeService,
'ora': StubOraService,
'comments': StubCommentsService,
'lti': StubLtiService,
'video': VideoSourceHttpService,

View File

@@ -1,282 +0,0 @@
"""
Unit tests for stub ORA implementation.
"""
import unittest
import requests
import json
from ..ora import StubOraService, StudentState
class StubOraServiceTest(unittest.TestCase):
def setUp(self):
"""
Start the stub server.
"""
super(StubOraServiceTest, self).setUp()
self.server = StubOraService()
self.addCleanup(self.server.shutdown)
def test_calibration(self):
# Ensure that we use the same student ID throughout
student_id = '1234'
# Initially, student should not be calibrated
response = requests.get(
self._peer_url('is_student_calibrated'),
params={'student_id': student_id, 'problem_id': '5678'}
)
self._assert_response(response, {
'version': 1, 'success': True,
'total_calibrated_on_so_far': 0,
'calibrated': False
})
# Retrieve a calibration essay
response = requests.get(
self._peer_url('show_calibration_essay'),
params={'student_id': student_id, 'problem_id': '5678'}
)
self._assert_response(response, {
'version': 1, 'success': True,
'submission_id': self.server.DUMMY_DATA['submission_id'],
'submission_key': self.server.DUMMY_DATA['submission_key'],
'student_response': self.server.DUMMY_DATA['student_response'],
'prompt': self.server.DUMMY_DATA['prompt'],
'rubric': self.server.DUMMY_DATA['rubric'],
'max_score': self.server.DUMMY_DATA['max_score']
})
# Grade the calibration essay
response = requests.post(
self._peer_url('save_calibration_essay'),
data={
'student_id': student_id,
'location': 'test location',
'calibration_essay_id': 1,
'score': 2,
'submission_key': 'key',
'feedback': 'Good job!'
}
)
self._assert_response(response, {
'version': 1, 'success': True,
'message': self.server.DUMMY_DATA['message'],
'actual_score': self.server.DUMMY_DATA['actual_score'],
'actual_rubric': self.server.DUMMY_DATA['actual_rubric'],
'actual_feedback': self.server.DUMMY_DATA['actual_feedback']
})
# Now the student should be calibrated
response = requests.get(
self._peer_url('is_student_calibrated'),
params={'student_id': student_id, 'problem_id': '5678'}
)
self._assert_response(response, {
'version': 1, 'success': True,
'total_calibrated_on_so_far': 1,
'calibrated': True
})
# But a student with a different ID should NOT be calibrated.
response = requests.get(
self._peer_url('is_student_calibrated'),
params={'student_id': 'another', 'problem_id': '5678'}
)
self._assert_response(response, {
'version': 1, 'success': True,
'total_calibrated_on_so_far': 0,
'calibrated': False
})
def test_grade_peers(self):
# Ensure a consistent student ID
student_id = '1234'
# Check initial number of submissions
# Should be none graded and 1 required
self._assert_num_graded(student_id, None, 0, 1)
# Register a problem that DOES have "peer" in the name
self._register_problem('test_location', 'Peer Assessed Problem')
# Retrieve the next submission
response = requests.get(
self._peer_url('get_next_submission'),
params={'grader_id': student_id, 'location': 'test_location'}
)
self._assert_response(response, {
'version': 1, 'success': True,
'submission_id': self.server.DUMMY_DATA['submission_id'],
'submission_key': self.server.DUMMY_DATA['submission_key'],
'student_response': self.server.DUMMY_DATA['student_response'],
'prompt': self.server.DUMMY_DATA['prompt'],
'rubric': self.server.DUMMY_DATA['rubric'],
'max_score': self.server.DUMMY_DATA['max_score']
})
# Grade the submission
response = requests.post(
self._peer_url('save_grade'),
data={
'location': 'test_location',
'grader_id': student_id,
'submission_id': 1,
'score': 2,
'feedback': 'Good job!',
'submission_key': 'key'
}
)
self._assert_response(response, {'version': 1, 'success': True})
# Check final number of submissions
# Shoud be one graded and none required
self._assert_num_graded(student_id, 'test_location', 1, 0)
# Grade the next submission the submission
response = requests.post(
self._peer_url('save_grade'),
data={
'location': 'test_location',
'grader_id': student_id,
'submission_id': 1,
'score': 2,
'feedback': 'Good job!',
'submission_key': 'key'
}
)
self._assert_response(response, {'version': 1, 'success': True})
# Check final number of submissions
# Shoud be two graded and none required
self._assert_num_graded(student_id, 'test_location', 2, 0)
def test_problem_list(self):
self._register_problem('test_location', 'Peer Grading Problem')
# The problem list returns dummy counts which are not updated
# The location we use is ignored by the LMS, and we ignore it in the stub,
# so we use a dummy value there too.
response = requests.get(
self._peer_url('get_problem_list'),
params={'course_id': 'test course'}
)
self._assert_response(response, {
'version': 1, 'success': True,
'problem_list': [{
'location': 'test_location',
'problem_name': 'Peer Grading Problem',
'num_graded': self.server.DUMMY_DATA['problem_list_num_graded'],
'num_pending': self.server.DUMMY_DATA['problem_list_num_pending'],
'num_required': self.server.DUMMY_DATA['problem_list_num_required']
}]
})
def test_ignore_non_peer_problem(self):
# Register a problem that does NOT have "peer" in the name
self._register_problem('test_location', 'Self Assessed Problem')
# Expect that the problem list is empty
response = requests.get(
self._peer_url('get_problem_list'),
params={'course_id': 'test course'}
)
self._assert_response(
response,
{'version': 1, 'success': True, 'problem_list': []}
)
# Expect that no data is available for the problem location
response = requests.get(
self._peer_url('get_data_for_location'),
params={'location': 'test_location', 'student_id': 'test'}
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {'version': 1, 'success': False})
def test_empty_problem_list(self):
# Without configuring any problem location, should return an empty list
response = requests.get(
self._peer_url('get_problem_list'),
params={'course_id': 'test course'}
)
self._assert_response(response, {'version': 1, 'success': True, 'problem_list': []})
def _peer_url(self, path):
"""
Construt a URL to the stub ORA peer-grading service.
"""
return "http://127.0.0.1:{port}/peer_grading/{path}/".format(
port=self.server.port, path=path
)
def _register_problem(self, location, name):
"""
Configure the stub to use a particular problem location
The actual implementation discovers problem locations by submission
to the XQueue; we do something similar by having the XQueue stub
register submitted locations with the ORA stub.
"""
grader_payload = json.dumps({'location': location, 'problem_id': name})
url = "http://127.0.0.1:{port}/test/register_submission".format(port=self.server.port)
response = requests.post(url, data={'grader_payload': grader_payload})
self.assertTrue(response.ok)
def _assert_response(self, response, expected_json):
"""
Assert that the `response` was successful and contained
`expected_json` (dict) as its content.
"""
self.assertTrue(response.ok)
self.assertEqual(response.json(), expected_json)
def _assert_num_graded(self, student_id, location, num_graded, num_required):
"""
ORA provides two distinct ways to get the submitted/graded counts.
Here we check both of them to ensure that the number that we've graded
is consistently `num_graded`.
"""
# Unlike the actual ORA service,
# we keep track of counts on a per-student basis.
# This means that every user starts with N essays to grade,
# and as they grade essays, that number decreases.
# We do NOT simulate students adding more essays to the queue,
# and essays that the current student submits are NOT graded
# by other students.
num_pending = StudentState.INITIAL_ESSAYS_AVAILABLE - num_graded
# Notifications
response = requests.get(
self._peer_url('get_notifications'),
params={'student_id': student_id, 'course_id': 'test course'}
)
self._assert_response(response, {
'version': 1, 'success': True,
'count_required': num_required,
'student_sub_count': self.server.DUMMY_DATA['student_sub_count'],
'count_graded': num_graded,
'count_available': num_pending
})
# Location data
if location is not None:
response = requests.get(
self._peer_url('get_data_for_location'),
params={'location': location, 'student_id': student_id}
)
self._assert_response(response, {
'version': 1, 'success': True,
'count_required': num_required,
'student_sub_count': self.server.DUMMY_DATA['student_sub_count'],
'count_graded': num_graded,
'count_available': num_pending
})

View File

@@ -115,19 +115,6 @@ class StubXQueueServiceTest(unittest.TestCase):
self.assertFalse(self.post.called)
self.assertTrue(logger.error.called)
def test_register_submission_url(self):
# Configure the XQueue stub to notify another service
# when it receives a submission.
register_url = 'http://127.0.0.1:8000/register_submission'
self.server.config['register_submission_url'] = register_url
callback_url = 'http://127.0.0.1:8000/test_callback'
submission = json.dumps({'grader_payload': 'test payload'})
self._post_submission(callback_url, 'test_queuekey', 'test_queue', submission)
# Check that a notification was sent
self.post.assert_any_call(register_url, data={'grader_payload': u'test payload'})
def _post_submission(self, callback_url, lms_key, queue_name, xqueue_body):
"""
Post a submission to the stub XQueue implementation.

View File

@@ -39,7 +39,8 @@ class StubXQueueHandler(StubHttpRequestHandler):
if self._is_grade_request():
# If configured, send the grader payload to other services.
self._register_submission(self.post_dict['xqueue_body'])
# TODO TNL-3906
# self._register_submission(self.post_dict['xqueue_body'])
try:
xqueue_header = json.loads(self.post_dict['xqueue_header'])

View File

@@ -461,7 +461,7 @@ def set_pipeline_timeout(strategy, user, *args, **kwargs):
# choice of the user.
def redirect_to_custom_form(request, auth_entry, user_details):
def redirect_to_custom_form(request, auth_entry, kwargs):
"""
If auth_entry is found in AUTH_ENTRY_CUSTOM, this is used to send provider
data to an external server's registration/login page.
@@ -469,13 +469,18 @@ def redirect_to_custom_form(request, auth_entry, user_details):
The data is sent as a base64-encoded values in a POST request and includes
a cryptographic checksum in case the integrity of the data is important.
"""
backend_name = request.backend.name
provider_id = provider.Registry.get_from_pipeline({'backend': backend_name, 'kwargs': kwargs}).provider_id
form_info = AUTH_ENTRY_CUSTOM[auth_entry]
secret_key = form_info['secret_key']
if isinstance(secret_key, unicode):
secret_key = secret_key.encode('utf-8')
custom_form_url = form_info['url']
data_str = json.dumps({
"user_details": user_details
"auth_entry": auth_entry,
"backend_name": backend_name,
"provider_id": provider_id,
"user_details": kwargs['details'],
})
digest = hmac.new(secret_key, msg=data_str, digestmod=hashlib.sha256).digest()
# Store the data in the session temporarily, then redirect to a page that will POST it to
@@ -537,7 +542,7 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia
raise AuthEntryError(backend, 'auth_entry is wrong. Settings requires a user.')
elif auth_entry in AUTH_ENTRY_CUSTOM:
# Pass the username, email, etc. via query params to the custom entry page:
return redirect_to_custom_form(strategy.request, auth_entry, kwargs['details'])
return redirect_to_custom_form(strategy.request, auth_entry, kwargs)
else:
raise AuthEntryError(backend, 'auth_entry invalid')

View File

@@ -80,13 +80,16 @@ class GoogleOauth2IntegrationTest(base.Oauth2IntegrationTest):
data_parsed = json.loads(data_decoded)
# The user's details get passed to the custom page as a base64 encoded query parameter:
self.assertEqual(data_parsed, {
'auth_entry': 'custom1',
'backend_name': 'google-oauth2',
'provider_id': 'oa2-google-oauth2',
'user_details': {
'username': 'email_value',
'email': 'email_value@example.com',
'fullname': 'name_value',
'first_name': 'given_name_value',
'last_name': 'family_name_value',
}
},
})
# Check the hash that is used to confirm the user's data in the GET parameter is correct
secret_key = settings.THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS['custom1']['secret_key']

View File

@@ -74,7 +74,7 @@ def post_to_custom_auth_form(request):
# Verify the format of pipeline_data:
data = {
'post_url': pipeline_data['post_url'],
# The user's name, email, etc. as base64 encoded JSON
# data: The provider info and user's name, email, etc. as base64 encoded JSON
# It's base64 encoded because it's signed cryptographically and we don't want whitespace
# or ordering issues affecting the hash/signature.
'data': pipeline_data['data'],

View File

@@ -19,7 +19,7 @@ class TestTrackViews(EventTrackingTestCase):
self.request_factory = RequestFactory()
patcher = patch('track.views.tracker')
patcher = patch('track.views.tracker', autospec=True)
self.mock_tracker = patcher.start()
self.addCleanup(patcher.stop)

View File

@@ -10,7 +10,7 @@ def add_organization(organization_data):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
if not organizations_enabled():
return None
from organizations import api as organizations_api
return organizations_api.add_organization(organization_data=organization_data)
@@ -20,7 +20,7 @@ def add_organization_course(organization_data, course_id):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
if not organizations_enabled():
return None
from organizations import api as organizations_api
return organizations_api.add_organization_course(organization_data=organization_data, course_key=course_id)
@@ -30,17 +30,31 @@ def get_organization(organization_id):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
if not organizations_enabled():
return []
from organizations import api as organizations_api
return organizations_api.get_organization(organization_id)
def get_organization_by_short_name(organization_short_name):
"""
Client API operation adapter/wrapper
"""
if not organizations_enabled():
return None
from organizations import api as organizations_api
from organizations.exceptions import InvalidOrganizationException
try:
return organizations_api.get_organization_by_short_name(organization_short_name)
except InvalidOrganizationException:
return None
def get_organizations():
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
if not organizations_enabled():
return []
from organizations import api as organizations_api
# Due to the way unit tests run for edx-platform, models are not yet available at the time
@@ -58,7 +72,7 @@ def get_organization_courses(organization_id):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
if not organizations_enabled():
return []
from organizations import api as organizations_api
return organizations_api.get_organization_courses(organization_id)
@@ -68,7 +82,14 @@ def get_course_organizations(course_id):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
if not organizations_enabled():
return []
from organizations import api as organizations_api
return organizations_api.get_course_organizations(course_id)
def organizations_enabled():
"""
Returns boolean indication if organizations app is enabled on not.
"""
return settings.FEATURES.get('ORGANIZATIONS_APP', False)

View File

@@ -1,17 +0,0 @@
"""
Utility function for some parsing stuff
"""
from xmodule.contentstore.content import StaticContent
def course_image_url(course):
"""
Return url of course image.
Args:
course(CourseDescriptor) : The course id to retrieve course image url.
Returns:
Absolute url of course image.
"""
loc = StaticContent.compute_location(course.id, course.course_image)
url = StaticContent.serialize_asset_key_with_slash(loc)
return url

View File

@@ -23,6 +23,7 @@ class OrganizationsHelpersTestCase(ModuleStoreTestCase):
self.organization = {
'name': 'Test Organization',
'short_name': 'Orgx',
'description': 'Testing Organization Helpers Library',
}
@@ -49,3 +50,25 @@ class OrganizationsHelpersTestCase(ModuleStoreTestCase):
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)
def test_get_organization_by_short_name_when_app_disabled(self):
"""
Tests get_organization_by_short_name api when app is disabled.
"""
response = organizations_helpers.get_organization_by_short_name(self.organization['short_name'])
self.assertIsNone(response)
@patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True})
def test_get_organization_by_short_name_when_app_enabled(self):
"""
Tests get_organization_by_short_name api when app is enabled.
"""
response = organizations_helpers.add_organization(organization_data=self.organization)
self.assertIsNotNone(response['id'])
response = organizations_helpers.get_organization_by_short_name(self.organization['short_name'])
self.assertIsNotNone(response['id'])
# fetch non existing org
response = organizations_helpers.get_organization_by_short_name('non_existing')
self.assertIsNone(response)

View File

@@ -233,7 +233,7 @@ def _record_feedback_in_zendesk(
new_ticket['ticket']['group_id'] = group['id']
try:
ticket_id = zendesk_api.create_ticket(new_ticket)
if group is None:
if group_name is not None and group is None:
# Support uses Zendesk groups to track tickets. In case we
# haven't been able to correctly group this ticket, log its ID
# so it can be found later.

View File

@@ -78,7 +78,8 @@ def to_latex(expr):
# substitute back into latex form for scripts
# literally something of the form
# 'scriptN' becomes '\\mathcal{N}'
# note: can't use something akin to the _print_hat method above because we sometimes get 'script(N)__B' or more complicated terms
# note: can't use something akin to the _print_hat method above because we
# sometimes get 'script(N)__B' or more complicated terms
expr_s = re.sub(
r'script([a-zA-Z0-9]+)',
'\\mathcal{\\1}',
@@ -99,11 +100,11 @@ def my_evalf(expr, chop=False):
if isinstance(expr, list):
try:
return [x.evalf(chop=chop) for x in expr]
except:
except Exception: # pylint: disable=broad-except
return expr
try:
return expr.evalf(chop=chop)
except:
except Exception: # pylint: disable=broad-except
return expr
@@ -115,23 +116,25 @@ def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False
if symtab:
varset = symtab
else:
varset = {'p': sympy.Symbol('p'),
'g': sympy.Symbol('g'),
'e': sympy.E, # for exp
'i': sympy.I, # lowercase i is also sqrt(-1)
'Q': sympy.Symbol('Q'), # otherwise it is a sympy "ask key"
'I': sympy.Symbol('I'), # otherwise it is sqrt(-1)
'N': sympy.Symbol('N'), # or it is some kind of sympy function
'ZZ': sympy.Symbol('ZZ'), # otherwise it is the PythonIntegerRing
'XI': sympy.Symbol('XI'), # otherwise it is the capital \XI
'hat': sympy.Function('hat'), # for unit vectors (8.02)
}
varset = {
'p': sympy.Symbol('p'),
'g': sympy.Symbol('g'),
'e': sympy.E, # for exp
'i': sympy.I, # lowercase i is also sqrt(-1)
'Q': sympy.Symbol('Q'), # otherwise it is a sympy "ask key"
'I': sympy.Symbol('I'), # otherwise it is sqrt(-1)
'N': sympy.Symbol('N'), # or it is some kind of sympy function
'ZZ': sympy.Symbol('ZZ'), # otherwise it is the PythonIntegerRing
'XI': sympy.Symbol('XI'), # otherwise it is the capital \XI
'hat': sympy.Function('hat'), # for unit vectors (8.02)
}
if do_qubit: # turn qubit(...) into Qubit instance
varset.update({'qubit': Qubit,
'Ket': Ket,
'dot': dot,
'bit': sympy.Function('bit'),
})
varset.update({
'qubit': Qubit,
'Ket': Ket,
'dot': dot,
'bit': sympy.Function('bit'),
})
if abcsym: # consider all lowercase letters as real symbols, in the parsing
for letter in string.lowercase:
if letter in varset: # exclude those already done
@@ -207,7 +210,7 @@ class formula(object):
usym = unicode(k.text)
try:
udata = unicodedata.name(usym)
except Exception:
except Exception: # pylint: disable=broad-except
udata = None
# print "usym = %s, udata=%s" % (usym,udata)
if udata: # eg "GREEK SMALL LETTER BETA"
@@ -271,7 +274,8 @@ class formula(object):
newk = etree.Element('mi')
newk.text = 'hat(%s)' % k[0].text
xml.replace(k, newk)
if gettag(k[0]) == 'mrow' and gettag(k[0][0]) == 'mi' and gettag(k[1]) == 'mo' and str(k[1].text) == '^':
if gettag(k[0]) == 'mrow' and gettag(k[0][0]) == 'mi' and \
gettag(k[1]) == 'mo' and str(k[1].text) == '^':
newk = etree.Element('mi')
newk.text = 'hat(%s)' % k[0][0].text
xml.replace(k, newk)
@@ -419,7 +423,7 @@ class formula(object):
# pre-process the presentation mathml before sending it to snuggletex to convert to content mathml
try:
xml = self.preprocess_pmathml(self.expr)
except Exception, err:
except Exception as err: # pylint: disable=broad-except
log.warning('Err %s while preprocessing; expr=%s', err, self.expr)
return "<html>Error! Cannot process pmathml</html>"
pmathml = etree.tostring(xml, pretty_print=True)
@@ -468,13 +472,6 @@ class formula(object):
def gettag(expr):
return re.sub('{http://[^}]+}', '', expr.tag)
# simple math
def op_divide(*args):
if not len(args) == 2:
raise Exception('divide given wrong number of arguments!')
# print "divide: arg0=%s, arg1=%s" % (args[0],args[1])
return sympy.Mul(args[0], sympy.Pow(args[1], -1))
def op_plus(*args):
return args[0] if len(args) == 1 else op_plus(*args[:-1]) + args[-1]
@@ -491,7 +488,7 @@ class formula(object):
opdict = {
'plus': op_plus,
'divide': operator.div, # should this be op_divide?
'divide': operator.div,
'times': op_times,
'minus': op_minus,
'root': sympy.sqrt,
@@ -518,12 +515,7 @@ class formula(object):
'ln': sympy.ln,
}
# simple symbols - TODO is this code used?
nums1dict = {
'pi': sympy.pi,
}
def parsePresentationMathMLSymbol(xml):
def parse_presentation_symbol(xml):
"""
Parse <msub>, <msup>, <mi>, and <mn>
"""
@@ -533,10 +525,10 @@ class formula(object):
elif tag == 'mi':
return xml.text
elif tag == 'msub':
return '_'.join([parsePresentationMathMLSymbol(y) for y in xml])
return '_'.join([parse_presentation_symbol(y) for y in xml])
elif tag == 'msup':
return '^'.join([parsePresentationMathMLSymbol(y) for y in xml])
raise Exception('[parsePresentationMathMLSymbol] unknown tag %s' % tag)
return '^'.join([parse_presentation_symbol(y) for y in xml])
raise Exception('[parse_presentation_symbol] unknown tag %s' % tag)
# parser tree for Content MathML
tag = gettag(xml)
@@ -574,11 +566,10 @@ class formula(object):
elif tag == 'cn': # number
return sympy.sympify(xml.text)
# return float(xml.text)
elif tag == 'ci': # variable (symbol)
if len(xml) > 0 and (gettag(xml[0]) == 'msub' or gettag(xml[0]) == 'msup'): # subscript or superscript
usym = parsePresentationMathMLSymbol(xml[0])
usym = parse_presentation_symbol(xml[0])
sym = sympy.Symbol(str(usym))
else:
usym = unicode(xml.text)
@@ -596,25 +587,22 @@ class formula(object):
sympy = property(make_sympy, None, None, 'sympy representation')
def GetContentMathML(self, asciimath, mathml):
def GetContentMathML(self, asciimath, mathml): # pylint: disable=invalid-name
"""
Handle requests to snuggletex API to convert the Ascii math to MathML
"""
# url = 'http://192.168.1.2:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
# url = 'http://127.0.0.1:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
url = 'https://math-xserver.mitx.mit.edu/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
if 1:
payload = {
'asciiMathInput': asciimath,
'asciiMathML': mathml,
#'asciiMathML':unicode(mathml).encode('utf-8'),
}
headers = {'User-Agent': "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13"}
request = requests.post(url, data=payload, headers=headers, verify=False)
request.encoding = 'utf-8'
ret = request.text
# print "encoding: ", request.encoding
payload = {
'asciiMathInput': asciimath,
'asciiMathML': mathml,
}
headers = {
'User-Agent': "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13"
}
request = requests.post(url, data=payload, headers=headers, verify=False)
request.encoding = 'utf-8'
ret = request.text
mode = 0
cmathml = []
@@ -629,153 +617,4 @@ class formula(object):
cmathml.append(k)
cmathml = '\n'.join(cmathml[2:])
cmathml = '<math xmlns="http://www.w3.org/1998/Math/MathML">\n' + unescape(cmathml) + '\n</math>'
# print cmathml
return cmathml
#-----------------------------------------------------------------------------
def test1():
"""Test XML strings - addition"""
xmlstr = """
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<plus/>
<cn>1</cn>
<cn>2</cn>
</apply>
</math>
"""
return formula(xmlstr)
def test2():
"""Test XML strings - addition, Greek alpha"""
xmlstr = u"""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<plus/>
<cn>1</cn>
<apply>
<times/>
<cn>2</cn>
<ci>α</ci>
</apply>
</apply>
</math>
"""
return formula(xmlstr)
def test3():
"""Test XML strings - addition, Greek gamma"""
xmlstr = """
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<divide/>
<cn>1</cn>
<apply>
<plus/>
<cn>2</cn>
<ci>γ</ci>
</apply>
</apply>
</math>
"""
return formula(xmlstr)
def test4():
"""Test XML strings - addition, Greek alpha, mfrac"""
xmlstr = u"""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>1</mn>
<mo>+</mo>
<mfrac>
<mn>2</mn>
<mi>α</mi>
</mfrac>
</mstyle>
</math>
"""
return formula(xmlstr)
def test5():
"""Test XML strings - sum of two matrices"""
xmlstr = u"""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mrow>
<mi>cos</mi>
<mrow>
<mo>(</mo>
<mi>&#x3B8;</mi>
<mo>)</mo>
</mrow>
</mrow>
<mo>&#x22C5;</mo>
<mrow>
<mo>[</mo>
<mtable>
<mtr>
<mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
</mtr>
<mtr>
<mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
<mo>+</mo>
<mrow>
<mo>[</mo>
<mtable>
<mtr>
<mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
</mtr>
<mtr>
<mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
</mstyle>
</math>
"""
return formula(xmlstr)
def test6():
"""Test XML strings - imaginary numbers"""
xmlstr = u"""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>1</mn>
<mo>+</mo>
<mi>i</mi>
</mstyle>
</math>
"""
return formula(xmlstr, options='imaginary')

View File

@@ -1,10 +1,8 @@
from setuptools import setup, find_packages
XMODULES = [
"abtest = xmodule.abtest_module:ABTestDescriptor",
"book = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"chapter = xmodule.seq_module:SequenceDescriptor",
"combinedopenended = xmodule.combined_open_ended_module:CombinedOpenEndedDescriptor",
"conditional = xmodule.conditional_module:ConditionalDescriptor",
"course = xmodule.course_module:CourseDescriptor",
"customtag = xmodule.template_module:CustomTagDescriptor",
@@ -13,7 +11,6 @@ XMODULES = [
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"library_content = xmodule.library_content_module:LibraryContentDescriptor",
"error = xmodule.error_module:ErrorDescriptor",
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
"poll_question = xmodule.poll_module:PollDescriptor",
"problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor",
@@ -36,7 +33,6 @@ XMODULES = [
"textannotation = xmodule.textannotation_module:TextAnnotationDescriptor",
"videoannotation = xmodule.videoannotation_module:VideoAnnotationDescriptor",
"imageannotation = xmodule.imageannotation_module:ImageAnnotationDescriptor",
"foldit = xmodule.foldit_module:FolditDescriptor",
"word_cloud = xmodule.word_cloud_module:WordCloudDescriptor",
"hidden = xmodule.hidden_module:HiddenDescriptor",
"raw = xmodule.raw_module:RawDescriptor",

View File

@@ -1,159 +0,0 @@
import random
import logging
from lxml import etree
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
from xblock.fields import String, Scope, Dict
DEFAULT = "_DEFAULT_GROUP"
log = logging.getLogger(__name__)
def group_from_value(groups, v):
"""
Given group: (('a', 0.3), ('b', 0.4), ('c', 0.3)) and random value v
in [0,1], return the associated group (in the above case, return
'a' if v < 0.3, 'b' if 0.3 <= v < 0.7, and 'c' if v > 0.7
"""
sum = 0
for (g, p) in groups:
sum = sum + p
if sum > v:
return g
# Round off errors might cause us to run to the end of the list.
# If the do, return the last element.
return g
class ABTestFields(object):
group_portions = Dict(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
group_assignments = Dict(help="What group this user belongs to", scope=Scope.preferences, default={})
group_content = Dict(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
has_children = True
class ABTestModule(ABTestFields, XModule):
"""
Implements an A/B test with an aribtrary number of competing groups
"""
def __init__(self, *args, **kwargs):
super(ABTestModule, self).__init__(*args, **kwargs)
if self.group is None:
self.group = group_from_value(
self.group_portions.items(),
random.uniform(0, 1)
)
@property
def group(self):
return self.group_assignments.get(self.experiment)
@group.setter
def group(self, value):
self.group_assignments[self.experiment] = value
@group.deleter
def group(self):
del self.group_assignments[self.experiment]
def get_child_descriptors(self):
active_locations = set(self.group_content[self.group])
return [desc for desc in self.descriptor.get_children() if desc.location.to_deprecated_string() in active_locations]
def displayable_items(self):
# Most modules return "self" as the displayable_item. We never display ourself
# (which is why we don't implement get_html). We only display our children.
return self.get_children()
# TODO (cpennington): Use Groups should be a first class object, rather than being
# managed by ABTests
class ABTestDescriptor(ABTestFields, RawDescriptor, XmlDescriptor):
module_class = ABTestModule
show_in_read_only_mode = True
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
XML Format:
<abtest experiment="experiment_name">
<group name="a" portion=".1"><contenta/></group>
<group name="b" portion=".2"><contentb/></group>
<default><contentdefault/></default>
</abtest>
"""
experiment = xml_object.get('experiment')
if experiment is None:
raise InvalidDefinitionError(
"ABTests must specify an experiment. Not found in:\n{xml}"
.format(xml=etree.tostring(xml_object, pretty_print=True)))
group_portions = {}
group_content = {}
children = []
for group in xml_object:
if group.tag == 'default':
name = DEFAULT
else:
name = group.get('name')
group_portions[name] = float(group.get('portion', 0))
child_content_urls = []
for child in group:
try:
child_block = system.process_xml(etree.tostring(child))
child_content_urls.append(child_block.scope_ids.usage_id)
except:
log.exception("Unable to load child when parsing ABTest. Continuing...")
continue
group_content[name] = child_content_urls
children.extend(child_content_urls)
default_portion = 1 - sum(
portion for (name, portion) in group_portions.items()
)
if default_portion < 0:
raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1")
group_portions[DEFAULT] = default_portion
children.sort()
return {
'group_portions': group_portions,
'group_content': group_content,
}, children
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('abtest')
xml_object.set('experiment', self.experiment)
for name, group in self.group_content.items():
if name == DEFAULT:
group_elem = etree.SubElement(xml_object, 'default')
else:
group_elem = etree.SubElement(xml_object, 'group', attrib={
'portion': str(self.group_portions[name]),
'name': name,
})
for child_loc in group:
child = self.system.load_item(child_loc)
self.runtime.add_block_as_child_node(child, group_elem)
return xml_object
def has_dynamic_children(self):
return True

View File

@@ -1125,9 +1125,6 @@ class CapaMixin(CapaFields):
self.attempts,
)
if hasattr(self.runtime, 'psychometrics_handler'): # update PsychometricsData using callback
self.runtime.psychometrics_handler(self.get_state_for_lcp())
# render problem into HTML
html = self.get_problem_html(encapsulate=False)
@@ -1375,10 +1372,6 @@ class CapaMixin(CapaFields):
event_info['attempts'] = self.attempts
self.track_function_unmask('problem_rescore', event_info)
# psychometrics should be called on rescoring requests in the same way as check-problem
if hasattr(self.runtime, 'psychometrics_handler'): # update PsychometricsData using callback
self.runtime.psychometrics_handler(self.get_state_for_lcp())
return {'success': success}
def save_problem(self, data):

View File

@@ -1,550 +0,0 @@
"""
ORA1. Deprecated.
"""
import logging
from lxml import etree
from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor
from .x_module import XModule, module_attr
from xblock.fields import Integer, Scope, String, List, Float, Boolean
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from xmodule.validation import StudioValidation, StudioValidationMessage
from collections import namedtuple
from .fields import Date, Timedelta
import textwrap
log = logging.getLogger("edx.courseware")
# Make '_' a no-op so we can scrape strings. Using lambda instead of
# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file
_ = lambda text: text
V1_SETTINGS_ATTRIBUTES = [
"display_name",
"max_attempts",
"graded",
"accept_file_upload",
"skip_spelling_checks",
"due",
"graceperiod",
"weight",
"min_to_calibrate",
"max_to_calibrate",
"peer_grader_count",
"required_peer_grading",
"peer_grade_finished_submissions_when_none_pending",
]
V1_STUDENT_ATTRIBUTES = [
"current_task_number",
"task_states",
"state",
"student_attempts",
"ready_to_reset",
"old_task_states",
]
V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES
VersionTuple = namedtuple('VersionTuple', ['descriptor', 'module', 'settings_attributes', 'student_attributes'])
VERSION_TUPLES = {
1: VersionTuple(CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES,
V1_STUDENT_ATTRIBUTES),
}
DEFAULT_VERSION = 1
DEFAULT_DATA = textwrap.dedent("""\
<combinedopenended>
<prompt>
<h3>Censorship in the Libraries</h3>
<p>'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author
</p>
<p>
Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.
</p>
</prompt>
<rubric>
<rubric>
<category>
<description>
Ideas
</description>
<option>
Difficult for the reader to discern the main idea. Too brief or too repetitive to establish or maintain a focus.
</option>
<option>
Attempts a main idea. Sometimes loses focus or ineffectively displays focus.
</option>
<option>
Presents a unifying theme or main idea, but may include minor tangents. Stays somewhat focused on topic and task.
</option>
<option>
Presents a unifying theme or main idea without going off on tangents. Stays completely focused on topic and task.
</option>
</category>
<category>
<description>
Content
</description>
<option>
Includes little information with few or no details or unrelated details. Unsuccessful in attempts to explore any facets of the topic.
</option>
<option>
Includes little information and few or no details. Explores only one or two facets of the topic.
</option>
<option>
Includes sufficient information and supporting details. (Details may not be fully developed; ideas may be listed.) Explores some facets of the topic.
</option>
<option>
Includes in-depth information and exceptional supporting details that are fully developed. Explores all facets of the topic.
</option>
</category>
<category>
<description>
Organization
</description>
<option>
Ideas organized illogically, transitions weak, and response difficult to follow.
</option>
<option>
Attempts to logically organize ideas. Attempts to progress in an order that enhances meaning, and demonstrates use of transitions.
</option>
<option>
Ideas organized logically. Progresses in an order that enhances meaning. Includes smooth transitions.
</option>
</category>
<category>
<description>
Style
</description>
<option>
Contains limited vocabulary, with many words used incorrectly. Demonstrates problems with sentence patterns.
</option>
<option>
Contains basic vocabulary, with words that are predictable and common. Contains mostly simple sentences (although there may be an attempt at more varied sentence patterns).
</option>
<option>
Includes vocabulary to make explanations detailed and precise. Includes varied sentence patterns, including complex sentences.
</option>
</category>
<category>
<description>
Voice
</description>
<option>
Demonstrates language and tone that may be inappropriate to task and reader.
</option>
<option>
Demonstrates an attempt to adjust language and tone to task and reader.
</option>
<option>
Demonstrates effective adjustment of language and tone to task and reader.
</option>
</category>
</rubric>
</rubric>
<task>
<selfassessment/></task>
<task>
<openended min_score_to_attempt="4" max_score_to_attempt="12" >
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
</openendedparam>
</openended>
</task>
<task>
<openended min_score_to_attempt="9" max_score_to_attempt="12" >
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "peer_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
</openendedparam>
</openended>
</task>
</combinedopenended>
""")
class VersionInteger(Integer):
"""
A model type that converts from strings to integers when reading from json.
Also does error checking to see if version is correct or not.
"""
def from_json(self, value):
try:
value = int(value)
if value not in VERSION_TUPLES:
version_error_string = "Could not find version {0}, using version {1} instead"
log.error(version_error_string.format(value, DEFAULT_VERSION))
value = DEFAULT_VERSION
except:
value = DEFAULT_VERSION
return value
class CombinedOpenEndedFields(object):
display_name = String(
display_name=_("Display Name"),
help=_("This name appears in the horizontal navigation at the top of the page."),
default=_("Open Response Assessment"),
scope=Scope.settings
)
current_task_number = Integer(
help=_("Current task that the student is on."),
default=0,
scope=Scope.user_state
)
old_task_states = List(
help=_("A list of lists of state dictionaries for student states that are saved. "
"This field is only populated if the instructor changes tasks after "
"the module is created and students have attempted it (for example, if a self assessed problem is "
"changed to self and peer assessed)."),
scope=Scope.user_state,
)
task_states = List(
help=_("List of state dictionaries of each task within this module."),
scope=Scope.user_state
)
state = String(
help=_("Which step within the current task that the student is on."),
default="initial",
scope=Scope.user_state
)
graded = Boolean(
display_name=_("Graded"),
help=_("Defines whether the student gets credit for this problem. Credit is based on peer grades of this problem."),
default=False,
scope=Scope.settings
)
student_attempts = Integer(
help=_("Number of attempts taken by the student on this problem"),
default=0,
scope=Scope.user_state
)
ready_to_reset = Boolean(
help=_("If the problem is ready to be reset or not."),
default=False,
scope=Scope.user_state
)
max_attempts = Integer(
display_name=_("Maximum Attempts"),
help=_("The number of times the student can try to answer this problem."),
default=1,
scope=Scope.settings,
values={"min": 1}
)
accept_file_upload = Boolean(
display_name=_("Allow File Uploads"),
help=_("Whether or not the student can submit files as a response."),
default=False,
scope=Scope.settings
)
skip_spelling_checks = Boolean(
display_name=_("Disable Quality Filter"),
help=_("If False, the Quality Filter is enabled and submissions with poor spelling, short length, or poor grammar will not be peer reviewed."),
default=False,
scope=Scope.settings
)
due = Date(
help=_("Date that this problem is due by"),
scope=Scope.settings
)
graceperiod = Timedelta(
help=_("Amount of time after the due date that submissions will be accepted"),
scope=Scope.settings
)
version = VersionInteger(
help=_("Current version number"),
default=DEFAULT_VERSION,
scope=Scope.settings)
data = String(
help=_("XML data for the problem"),
scope=Scope.content,
default=DEFAULT_DATA)
weight = Float(
display_name=_("Problem Weight"),
help=_("Defines the number of points each problem is worth. If the value is not set, each problem is worth one point."),
scope=Scope.settings,
values={"min": 0, "step": ".1"},
default=1
)
min_to_calibrate = Integer(
display_name=_("Minimum Peer Grading Calibrations"),
help=_("The minimum number of calibration essays each student will need to complete for peer grading."),
default=3,
scope=Scope.settings,
values={"min": 1, "max": 20, "step": "1"}
)
max_to_calibrate = Integer(
display_name=_("Maximum Peer Grading Calibrations"),
help=_("The maximum number of calibration essays each student will need to complete for peer grading."),
default=6,
scope=Scope.settings,
values={"min": 1, "max": 20, "step": "1"}
)
peer_grader_count = Integer(
display_name=_("Peer Graders per Response"),
help=_("The number of peers who will grade each submission."),
default=3,
scope=Scope.settings,
values={"min": 1, "step": "1", "max": 5}
)
required_peer_grading = Integer(
display_name=_("Required Peer Grading"),
help=_("The number of other students each student making a submission will have to grade."),
default=3,
scope=Scope.settings,
values={"min": 1, "step": "1", "max": 5}
)
peer_grade_finished_submissions_when_none_pending = Boolean(
display_name=_('Allow "overgrading" of peer submissions'),
help=_(
"EXPERIMENTAL FEATURE. Allow students to peer grade submissions that already have the requisite number of graders, "
"but ONLY WHEN all submissions they are eligible to grade already have enough graders. "
"This is intended for use when settings for `Required Peer Grading` > `Peer Graders per Response`"
),
default=False,
scope=Scope.settings,
)
markdown = String(
help=_("Markdown source of this module"),
default=textwrap.dedent("""\
[prompt]
<h3>Censorship in the Libraries</h3>
<p>'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author
</p>
<p>
Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.
</p>
[prompt]
[rubric]
+ Ideas
- Difficult for the reader to discern the main idea. Too brief or too repetitive to establish or maintain a focus.
- Attempts a main idea. Sometimes loses focus or ineffectively displays focus.
- Presents a unifying theme or main idea, but may include minor tangents. Stays somewhat focused on topic and task.
- Presents a unifying theme or main idea without going off on tangents. Stays completely focused on topic and task.
+ Content
- Includes little information with few or no details or unrelated details. Unsuccessful in attempts to explore any facets of the topic.
- Includes little information and few or no details. Explores only one or two facets of the topic.
- Includes sufficient information and supporting details. (Details may not be fully developed; ideas may be listed.) Explores some facets of the topic.
- Includes in-depth information and exceptional supporting details that are fully developed. Explores all facets of the topic.
+ Organization
- Ideas organized illogically, transitions weak, and response difficult to follow.
- Attempts to logically organize ideas. Attempts to progress in an order that enhances meaning, and demonstrates use of transitions.
- Ideas organized logically. Progresses in an order that enhances meaning. Includes smooth transitions.
+ Style
- Contains limited vocabulary, with many words used incorrectly. Demonstrates problems with sentence patterns.
- Contains basic vocabulary, with words that are predictable and common. Contains mostly simple sentences (although there may be an attempt at more varied sentence patterns).
- Includes vocabulary to make explanations detailed and precise. Includes varied sentence patterns, including complex sentences.
+ Voice
- Demonstrates language and tone that may be inappropriate to task and reader.
- Demonstrates an attempt to adjust language and tone to task and reader.
- Demonstrates effective adjustment of language and tone to task and reader.
[rubric]
[tasks]
(Self), ({4-12}AI), ({9-12}Peer)
[tasks]
"""),
scope=Scope.settings
)
class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
"""
This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).
It transitions between problems, and support arbitrary ordering.
Each combined open ended module contains one or multiple "child" modules.
Child modules track their own state, and can transition between states. They also implement get_html and
handle_ajax.
The combined open ended module transitions between child modules as appropriate, tracks its own state, and passess
ajax requests from the browser to the child module or handles them itself (in the cases of reset and next problem)
ajax actions implemented by all children are:
'save_answer' -- Saves the student answer
'save_assessment' -- Saves the student assessment (or external grader assessment)
'save_post_assessment' -- saves a post assessment (hint, feedback on feedback, etc)
ajax actions implemented by combined open ended module are:
'reset' -- resets the whole combined open ended module and returns to the first child module
'next_problem' -- moves to the next child module
'get_results' -- gets results from a given child module
Types of children. Task is synonymous with child module, so each combined open ended module
incorporates multiple children (tasks):
openendedmodule
selfassessmentmodule
CombinedOpenEndedModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__
"""
STATE_VERSION = 1
# states
INITIAL = 'initial'
ASSESSING = 'assessing'
INTERMEDIATE_DONE = 'intermediate_done'
DONE = 'done'
icon_class = 'problem'
js = {
'coffee': [
resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
],
'js': [
resource_string(__name__, 'js/src/collapsible.js'),
]
}
js_module_name = "CombinedOpenEnded"
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
def __init__(self, *args, **kwargs):
"""
Definition file should have one or many task blocks, a rubric block, and a prompt block.
See DEFAULT_DATA for a sample.
"""
super(CombinedOpenEndedModule, self).__init__(*args, **kwargs)
self.system.set('location', self.location)
if self.task_states is None:
self.task_states = []
if self.old_task_states is None:
self.old_task_states = []
version_tuple = VERSION_TUPLES[self.version]
self.student_attributes = version_tuple.student_attributes
self.settings_attributes = version_tuple.settings_attributes
attributes = self.student_attributes + self.settings_attributes
static_data = {}
instance_state = {k: getattr(self, k) for k in attributes}
self.child_descriptor = version_tuple.descriptor(self.system)
self.child_definition = version_tuple.descriptor.definition_from_xml(etree.fromstring(self.data), self.system)
self.child_module = version_tuple.module(self.system, self.location, self.child_definition, self.child_descriptor,
instance_state=instance_state, static_data=static_data,
attributes=attributes)
self.save_instance_data()
def get_html(self):
self.save_instance_data()
return_value = self.child_module.get_html()
return return_value
def handle_ajax(self, dispatch, data):
self.save_instance_data()
return_value = self.child_module.handle_ajax(dispatch, data)
self.save_instance_data()
return return_value
def get_instance_state(self):
return self.child_module.get_instance_state()
def get_score(self):
return self.child_module.get_score()
def max_score(self):
return self.child_module.max_score()
def get_progress(self):
return self.child_module.get_progress()
@property
def due_date(self):
return self.child_module.due_date
def save_instance_data(self):
for attribute in self.student_attributes:
setattr(self, attribute, getattr(self.child_module, attribute))
def validate(self):
"""
Message for either error or warning validation message/s.
Returns message and type. Priority given to error type message.
"""
return self.descriptor.validate()
class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
"""
Module for adding combined open ended questions
"""
mako_template = "widgets/open-ended-edit.html"
module_class = CombinedOpenEndedModule
has_score = True
always_recalculate_grades = True
template_dir_name = "combinedopenended"
#Specify whether or not to pass in S3 interface
needs_s3_interface = True
#Specify whether or not to pass in open ended interface
needs_open_ended_interface = True
js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/edit.coffee')]}
js_module_name = "OpenEndedMarkdownEditingDescriptor"
css = {'scss': [resource_string(__name__, 'css/editor/edit.scss'), resource_string(__name__, 'css/combinedopenended/edit.scss')]}
metadata_translations = {
'is_graded': 'graded',
'attempts': 'max_attempts',
}
def get_context(self):
_context = RawDescriptor.get_context(self)
_context.update({'markdown': self.markdown,
'enable_markdown': self.markdown is not None})
return _context
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod,
CombinedOpenEndedDescriptor.markdown, CombinedOpenEndedDescriptor.version])
return non_editable_fields
# Proxy to CombinedOpenEndedModule so that external callers don't have to know if they're working
# with a module or a descriptor
child_module = module_attr('child_module')
def validate(self):
"""
Validates the state of this instance. This is the override of the general XBlock method,
and it will also ask its superclass to validate.
"""
validation = super(CombinedOpenEndedDescriptor, self).validate()
validation = StudioValidation.copy(validation)
i18n_service = self.runtime.service(self, "i18n")
validation.summary = StudioValidationMessage(
StudioValidationMessage.ERROR,
i18n_service.ugettext(
"ORA1 is no longer supported. To use this assessment, "
"replace this ORA1 component with an ORA2 component."
)
)
return validation

View File

@@ -1,52 +1,50 @@
"""
MongoDB/GridFS-level code for the contentstore.
"""
import os
import json
import pymongo
import gridfs
from gridfs.errors import NoFile
from xmodule.contentstore.content import XASSET_LOCATION_TAG
import logging
from .content import StaticContent, ContentStore, StaticContentStream
from xmodule.exceptions import NotFoundError
from fs.osfs import OSFS
import os
import json
from bson.son import SON
from mongodb_proxy import autoretry_read
from opaque_keys.edx.keys import AssetKey
from xmodule.contentstore.content import XASSET_LOCATION_TAG
from xmodule.exceptions import NotFoundError
from xmodule.modulestore.django import ASSET_IGNORE_REGEX
from xmodule.util.misc import escape_invalid_characters
from xmodule.mongo_connection import connect_to_mongodb
from .content import StaticContent, ContentStore, StaticContentStream
class MongoContentStore(ContentStore):
# pylint: disable=unused-argument
def __init__(self, host, db, port=27017, user=None, password=None, bucket='fs', collection=None, **kwargs):
"""
MongoDB-backed ContentStore.
"""
# pylint: disable=unused-argument, bad-continuation
def __init__(
self, host, db,
port=27017, tz_aware=True, user=None, password=None, bucket='fs', collection=None, **kwargs
):
"""
Establish the connection with the mongo backend and connect to the collections
:param collection: ignores but provided for consistency w/ other doc_store_config patterns
"""
logging.debug('Using MongoDB for static content serving at host={0} port={1} db={2}'.format(host, port, db))
# Remove the replicaSet parameter.
kwargs.pop('replicaSet', None)
_db = pymongo.database.Database(
pymongo.MongoClient(
host=host,
port=port,
document_class=dict,
**kwargs
),
db
# GridFS will throw an exception if the Database is wrapped in a MongoProxy. So don't wrap it.
# The appropriate methods below are marked as autoretry_read - those methods will handle
# the AutoReconnect errors.
proxy = False
mongo_db = connect_to_mongodb(
db, host,
port=port, tz_aware=tz_aware, user=user, password=password, proxy=proxy, **kwargs
)
if user is not None and password is not None:
_db.authenticate(user, password)
self.fs = gridfs.GridFS(mongo_db, bucket) # pylint: disable=invalid-name
self.fs = gridfs.GridFS(_db, bucket)
self.fs_files = _db[bucket + ".files"] # the underlying collection GridFS uses
self.fs_files = mongo_db[bucket + ".files"] # the underlying collection GridFS uses
def close_connections(self):
"""
@@ -86,11 +84,15 @@ class MongoContentStore(ContentStore):
return content
def delete(self, location_or_id):
"""
Delete an asset.
"""
if isinstance(location_or_id, AssetKey):
location_or_id, _ = self.asset_db_key(location_or_id)
# Deletes of non-existent files are considered successful
self.fs.delete(location_or_id)
@autoretry_read()
def find(self, location, throw_on_not_found=True, as_stream=False):
content_id, __ = self.asset_db_key(location)
@@ -206,6 +208,7 @@ class MongoContentStore(ContentStore):
self.fs_files.remove(query)
return assets_to_delete
@autoretry_read()
def _get_all_content_for_course(self,
course_key,
get_thumbnails=False,
@@ -288,6 +291,7 @@ class MongoContentStore(ContentStore):
if not result.get('updatedExisting', True):
raise NotFoundError(asset_db_key)
@autoretry_read()
def get_attrs(self, location):
"""
Gets all of the attributes associated with the given asset. Note, returns even built in attrs

View File

@@ -5,9 +5,10 @@ This is a place to put simple functions that operate on course metadata. It
allows us to share code between the CourseDescriptor and CourseOverview
classes, which both need these type of functions.
"""
from datetime import datetime
from datetime import timedelta
from base64 import b32encode
from datetime import datetime, timedelta
import dateutil.parser
from math import exp
from django.utils.timezone import UTC
@@ -222,3 +223,43 @@ def may_certify_for_course(certificates_display_behavior, certificates_show_befo
or certificates_show_before_end
)
return show_early or has_ended
def sorting_score(start, advertised_start, announcement):
"""
Returns a tuple that can be used to sort the courses according
to how "new" they are. The "newness" score is computed using a
heuristic that takes into account the announcement and
(advertised) start dates of the course if available.
The lower the number the "newer" the course.
"""
# Make courses that have an announcement date have a lower
# score than courses than don't, older courses should have a
# higher score.
announcement, start, now = sorting_dates(start, advertised_start, announcement)
scale = 300.0 # about a year
if announcement:
days = (now - announcement).days
score = -exp(-days / scale)
else:
days = (now - start).days
score = exp(days / scale)
return score
def sorting_dates(start, advertised_start, announcement):
"""
Utility function to get datetime objects for dates used to
compute the is_new flag and the sorting_score.
"""
try:
start = dateutil.parser.parse(advertised_start)
if start.tzinfo is None:
start = start.replace(tzinfo=UTC())
except (ValueError, AttributeError):
start = start
now = datetime.now(UTC())
return announcement, start, now

View File

@@ -3,12 +3,10 @@ Django module container for classes and operations related to the "Course Module
"""
import logging
from cStringIO import StringIO
from math import exp
from lxml import etree
from path import Path as path
import requests
from datetime import datetime
import dateutil.parser
from lazy import lazy
from xmodule import course_metadata_utils
@@ -416,210 +414,6 @@ class CourseFields(object):
scope=Scope.settings
)
has_children = True
checklists = List(
help=_("Checklist to Follow When Developing a Course"),
scope=Scope.settings,
default=[
{
"short_description": _("Getting Started With Studio"),
"items": [
{
"short_description": _("Add Course Team Members"),
"long_description": _(
"Grant your collaborators permission to edit your course so you can work together."
),
"is_checked": False,
"action_url": "ManageUsers",
"action_text": _("Edit Course Team"),
"action_external": False,
},
{
"short_description": _("Set Important Dates for Your Course"),
"long_description": _(
"Establish your course's student enrollment and launch dates on the Schedule and Details "
"page."
),
"is_checked": False,
"action_url": "SettingsDetails",
"action_text": _("Edit Course Details &amp; Schedule"),
"action_external": False,
},
{
"short_description": _("Draft Your Course's Grading Policy"),
"long_description": _(
"Set up your assignment types and grading policy even if you haven't created all your "
"assignments."
),
"is_checked": False,
"action_url": "SettingsGrading",
"action_text": _("Edit Grading Settings"),
"action_external": False,
},
{
"short_description": _("Explore the Other Studio Checklists"),
"long_description": _(
"Discover other available course authoring tools, and find help when you need it."
),
"is_checked": False,
"action_url": "",
"action_text": "",
"action_external": False,
},
],
},
{
"short_description": _("Draft a Rough Course Outline"),
"items": [
{
"short_description": _("Create Your First Section and Subsection"),
"long_description": _("Use your course outline to build your first Section and Subsection."),
"is_checked": False,
"action_url": "CourseOutline",
"action_text": _("Edit Course Outline"),
"action_external": False,
},
{
"short_description": _("Set Section Release Dates"),
"long_description": _(
"Specify the release dates for each Section in your course. Sections become visible to "
"students on their release dates."
),
"is_checked": False,
"action_url": "CourseOutline",
"action_text": _("Edit Course Outline"),
"action_external": False,
},
{
"short_description": _("Designate a Subsection as Graded"),
"long_description": _(
"Set a Subsection to be graded as a specific assignment type. Assignments within graded "
"Subsections count toward a student's final grade."
),
"is_checked": False,
"action_url": "CourseOutline",
"action_text": _("Edit Course Outline"),
"action_external": False,
},
{
"short_description": _("Reordering Course Content"),
"long_description": _("Use drag and drop to reorder the content in your course."),
"is_checked": False,
"action_url": "CourseOutline",
"action_text": _("Edit Course Outline"),
"action_external": False,
},
{
"short_description": _("Renaming Sections"),
"long_description": _("Rename Sections by clicking the Section name from the Course Outline."),
"is_checked": False,
"action_url": "CourseOutline",
"action_text": _("Edit Course Outline"),
"action_external": False,
},
{
"short_description": _("Deleting Course Content"),
"long_description": _(
"Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is "
"no Undo function."
),
"is_checked": False,
"action_url": "CourseOutline",
"action_text": _("Edit Course Outline"),
"action_external": False,
},
{
"short_description": _("Add an Instructor-Only Section to Your Outline"),
"long_description": _(
"Some course authors find using a section for unsorted, in-progress work useful. To do "
"this, create a section and set the release date to the distant future."
),
"is_checked": False,
"action_url": "CourseOutline",
"action_text": _("Edit Course Outline"),
"action_external": False,
},
],
},
{
"short_description": _("Explore edX's Support Tools"),
"items": [
{
"short_description": _("Explore the Studio Help Forum"),
"long_description": _(
"Access the Studio Help forum from the menu that appears when you click your user name "
"in the top right corner of Studio."
),
"is_checked": False,
"action_url": "http://help.edge.edx.org/",
"action_text": _("Visit Studio Help"),
"action_external": True,
},
{
"short_description": _("Enroll in edX 101"),
"long_description": _("Register for edX 101, edX's primer for course creation."),
"is_checked": False,
"action_url": "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about",
"action_text": _("Register for edX 101"),
"action_external": True,
},
{
"short_description": _("Download the Studio Documentation"),
"long_description": _("Download the searchable Studio reference documentation in PDF form."),
"is_checked": False,
"action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf",
"action_text": _("Download Documentation"),
"action_external": True,
},
],
},
{
"short_description": _("Draft Your Course About Page"),
"items": [
{
"short_description": _("Draft a Course Description"),
"long_description": _(
"Courses on edX have an About page that includes a course video, description, and more. "
"Draft the text students will read before deciding to enroll in your course."
),
"is_checked": False,
"action_url": "SettingsDetails",
"action_text": _("Edit Course Schedule &amp; Details"),
"action_external": False,
},
{
"short_description": _("Add Staff Bios"),
"long_description": _(
"Showing prospective students who their instructor will be is helpful. "
"Include staff bios on the course About page."
),
"is_checked": False,
"action_url": "SettingsDetails",
"action_text": _("Edit Course Schedule &amp; Details"),
"action_external": False,
},
{
"short_description": _("Add Course FAQs"),
"long_description": _("Include a short list of frequently asked questions about your course."),
"is_checked": False,
"action_url": "SettingsDetails",
"action_text": _("Edit Course Schedule &amp; Details"),
"action_external": False,
},
{
"short_description": _("Add Course Prerequisites"),
"long_description": _(
"Let students know what knowledge and/or skills they should have before "
"they enroll in your course."
),
"is_checked": False,
"action_url": "SettingsDetails",
"action_text": _("Edit Course Schedule &amp; Details"),
"action_external": False,
},
],
},
],
)
info_sidebar_name = String(
display_name=_("Course Info Sidebar Name"),
help=_(
@@ -1264,7 +1058,9 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
flag = self.is_new
if flag is None:
# Use a heuristic if the course has not been flagged
announcement, start, now = self._sorting_dates()
announcement, start, now = course_metadata_utils.sorting_dates(
self.start, self.advertised_start, self.announcement
)
if announcement and (now - announcement).days < 30:
# The course has been announced for less that month
return True
@@ -1284,41 +1080,11 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
Returns a tuple that can be used to sort the courses according
the how "new" they are. The "newness" score is computed using a
heuristic that takes into account the announcement and
(advertized) start dates of the course if available.
(advertised) start dates of the course if available.
The lower the number the "newer" the course.
"""
# Make courses that have an announcement date shave a lower
# score than courses than don't, older courses should have a
# higher score.
announcement, start, now = self._sorting_dates()
scale = 300.0 # about a year
if announcement:
days = (now - announcement).days
score = -exp(-days / scale)
else:
days = (now - start).days
score = exp(days / scale)
return score
def _sorting_dates(self):
# utility function to get datetime objects for dates used to
# compute the is_new flag and the sorting_score
announcement = self.announcement
if announcement is not None:
announcement = announcement
try:
start = dateutil.parser.parse(self.advertised_start)
if start.tzinfo is None:
start = start.replace(tzinfo=UTC())
except (ValueError, AttributeError):
start = self.start
now = datetime.now(UTC())
return announcement, start, now
return course_metadata_utils.sorting_score(self.start, self.advertised_start, self.announcement)
@lazy
def grading_context(self):

View File

@@ -23,8 +23,8 @@
// ====================
$annotation-yellow: rgba(255,255,10,0.3);
$color-copy-tip: rgb(100,100,100);
$correct: $green-d1;
$partiallycorrect: $green-d1;
$correct: $green-d2;
$partiallycorrect: $green-d2;
$incorrect: $red;
// +Extends - Capa
@@ -972,7 +972,7 @@ div.problem {
.detailed-solution {
> p:first-child {
@extend %t-strong;
color: #aaa;
color: $gray;
text-transform: uppercase;
font-style: normal;
font-size: 0.9em;

View File

@@ -1,993 +0,0 @@
// lms - xmodule - combinedopenended
// ====================
h2 {
margin-top: 0;
margin-bottom: ($baseline*0.75);
&.problem-header {
section.staff {
margin-top: ($baseline*1.5);
font-size: 80%;
}
}
@media print {
display: block;
width: auto;
border-right: 0;
}
}
// Problem Header
div.name{
padding-bottom: ($baseline*0.75);
h2 {
display: inline;
}
.progress-container {
display: inline;
float: right;
padding-top: 3px;
}
}
.inline-error {
color: darken($error-color, 10%);
}
section.combined-open-ended {
@include clearfix();
.written-feedback {
position: relative;
margin: 0;
height: 150px;
border: 1px solid lightgray;
padding: ($baseline/4);
resize: vertical;
width: 99%;
overflow: auto;
.del {
text-decoration: line-through;
background-color: #ffc3c3;
}
.ins {
background-color: #c3ffc3;
}
}
}
div.problemwrapper {
border: 1px solid lightgray;
border-radius: ($baseline/2);
.status-bar {
background-color: #eee;
border-radius: ($baseline/2) ($baseline/2) 0 0;
border-bottom: 1px solid lightgray;
.statustable {
width: 100%;
padding: $baseline;
}
.status-container {
display: table-cell;
text-align: center;
.status-elements {
border-radius: ($baseline/4);
border: 1px solid lightgray;
}
}
.problemtype-container {
padding: ($baseline/2);
width: 60%;
}
.problemtype{
padding: ($baseline/2);
}
.assessments-container {
float: right;
padding: ($baseline/2) $baseline ($baseline/2) ($baseline/2);
.assessment-text {
display: inline-block;
display: table-cell;
padding-right: ($baseline/2);
}
}
}
.item-container {
padding-bottom: ($baseline/2);
margin: 15px;
}
.result-container {
float: left;
width: 100%;
position: relative;
}
}
section.legend-container {
margin: 15px;
border-radius: ($baseline/4);
.legenditem {
display: inline;
padding: ($baseline/2);
width: 20%;
background-color: #eee;
font-size: .9em;
}
}
section.combined-open-ended-status {
vertical-align: center;
.statusitem {
display: table-cell;
padding: ($baseline/2);
width: 30px;
border-right: 1px solid lightgray;
background-color: #eee;
color: #2c2c2c;
font-size: .9em;
&:first-child {
border-radius: ($baseline/4) 0 0 ($baseline/4);
}
&:last-child {
border-right: 0;
border-radius: 0 ($baseline/4) ($baseline/4) 0;
}
&:only-child {
border-radius: ($baseline/4);
}
.show-results {
margin-top: .3em;
text-align:right;
}
.show-results-button {
font: 1em monospace;
}
}
.statusitem-current {
background-color: $white;
color: #222;
}
span {
&.unanswered {
display: inline-block;
position: relative;
float: right;
width: 14px;
height: 14px;
background: url('#{$static-path}/images/unanswered-icon.png') center center no-repeat;
}
&.correct {
display: inline-block;
position: relative;
float: right;
width: 25px;
height: 20px;
background: url('#{$static-path}/images/correct-icon.png') center center no-repeat;
}
&.incorrect {
display: inline-block;
position: relative;
float: right;
width: 20px;
height: 20px;
background: url('#{$static-path}/images/incorrect-icon.png') center center no-repeat;
}
}
.icon-caret-right {
display: inline-block;
margin-right: ($baseline/4);
vertical-align: baseline;
}
}
// Problem Section Controls
.visibility-control, .visibility-control-prompt {
display: block;
width: 100%;
height: 40px;
.inner {
float: left;
margin-top: $baseline;
width: 85%;
height: 5px;
border-top: 1px dotted #ddd;
}
}
.section-header {
display: block;
float: right;
padding-top: ($baseline/2);
width: 15%;
text-align: center;
font-size: .9em;
}
// Rubric Styling
.wrapper-score-selection {
display: table-cell;
padding: 0 ($baseline/2);
width: 20px;
vertical-align: middle;
}
.wrappable {
display: table-cell;
padding: ($baseline/4);
}
.rubric-list-item {
margin-bottom: ($baseline/10);
padding: ($baseline/2);
&:hover, &:focus {
background-color: #eee;
}
.rubric-label-selected{
border-radius: ($baseline/4);
background-color: #eee;
}
}
span.rubric-category {
display: block;
margin-bottom: ($baseline/2);
padding-top: ($baseline/2);
width: 100%;
border-bottom: 1px solid lightgray;
font-size: 1.1em;
}
div.combined-rubric-container {
margin: 15px;
padding-top: ($baseline/2);
padding-bottom: ($baseline/4);
ul.rubric-list {
margin: 0 $baseline ($baseline/2) $baseline;
padding: 0;
list-style-type: none;
li {
&.rubric-list-item {
margin-bottom: ($baseline/10);
padding: ($baseline/2);
}
}
}
h4 {
padding-top: ($baseline/2);
}
span.rubric-category {
display: block;
width: 100%;
border-bottom: 1px solid lightgray;
font-weight: bold;
font-size: .9em;
}
label.choicegroup_correct {
&:before {
margin-right: ($baseline*0.75);
content: url('#{$static-path}/images/correct-icon.png');
}
}
label.choicegroup_partialcorrect {
&:before {
margin-right: ($baseline*0.75);
content: url('#{$static-path}/images/partially-correct-icon.png');
}
}
label.choicegroup_incorrect {
&:before {
margin-right: ($baseline*0.75);
content: url('#{$static-path}/images/incorrect-icon.png');
}
}
div.written-feedback {
background: $gray-l6;
padding: ($baseline/4);
}
}
div.result-container {
padding-top: ($baseline/2);
padding-bottom: ($baseline/4);
.evaluation {
p {
margin-bottom: 1px;
}
}
.feedback-on-feedback {
height: 100px;
margin-right: 0;
}
.evaluation-response {
margin-bottom: ($baseline/10);
header {
a {
font-size: .85em;
}
}
}
.evaluation-scoring {
.scoring-list {
margin-left: 3px;
list-style-type: none;
li {
display:inline;
margin-left: 0;
&:first-child {
margin-left: 0;
}
label {
font-size: .9em;
}
}
}
}
.submit-message-container {
margin: ($baseline/2) 0;
}
.external-grader-message {
margin-bottom: ($baseline/4);
section {
padding-left: $baseline;
background-color: #fafafa;
color: #2c2c2c;
font-family: monospace;
font-size: 1em;
padding-top: ($baseline/2);
padding-bottom: 30px;
header {
font-size: 1.4em;
}
.shortform {
font-weight: bold;
}
.longform {
padding: 0;
margin: 0;
.result-errors {
margin: ($baseline/4);
padding: ($baseline/2) ($baseline/2) ($baseline/2) ($baseline*2);
background: url('#{$static-path}/images/incorrect-icon.png') center left no-repeat;
li {
color: #B00;
}
}
.result-output {
margin: ($baseline/4);
padding: $baseline 0 ($baseline*0.75) ($baseline*2.5);
border-top: 1px solid #ddd;
border-left: 20px solid #fafafa;
h4 {
font-size: 1em;
font-family: monospace;
}
dl {
margin: 0;
}
dt {
margin-top: $baseline;
}
dd {
margin-left: 24pt;
}
}
.markup-text{
margin: ($baseline/4);
padding: $baseline 0 ($baseline*0.75) ($baseline*2.5);
border-top: 1px solid #ddd;
border-left: 20px solid #fafafa;
bs {
color: #bb0000;
}
bg {
color: #bda046;
}
}
}
}
}
.rubric-result-container {
padding: ($baseline/10);
margin: 0;
display: inline;
.rubric-result {
font-size: .9em;
padding: ($baseline/10);
display: inline-table;
}
}
}
div.rubric {
ul.rubric-list{
margin: 0 $baseline ($baseline/2) $baseline;
padding: 0;
list-style: none;
list-style-type: none;
li {
&.rubric-list-item {
margin-bottom: ($baseline/10);
padding: ($baseline/2);
border-radius: ($baseline/4);
&:hover, &:focus {
background-color: #eee;
}
.wrapper-score-selection {
display: table-cell;
padding: 0 ($baseline/2);
width: 20px;
vertical-align: middle;
}
.wrappable {
display: table-cell;
padding: ($baseline/4);
}
}
}
}
span.rubric-category {
display: block;
width: 100%;
border-bottom: 1px solid lightgray;
font-weight: bold;
font-size: .9em;
}
}
section.open-ended-child {
@media print {
display: block;
padding: 0;
width: auto;
canvas, img {
page-break-inside: avoid;
}
}
.inline {
display: inline;
}
ol.enumerate {
li {
&:before {
display: block;
visibility: hidden;
height: 0;
content: " ";
}
}
}
.solution-span {
> span {
position: relative;
display: block;
margin: $baseline 0;
padding: 9px 15px $baseline;
border: 1px solid #ddd;
border-radius: 3px;
background: $white;
box-shadow: inset 0 0 0 1px #eee;
&:empty {
display: none;
}
}
}
p {
&.answer {
margin-top: -2px;
}
&.status {
margin: 8px 0 0 ($baseline/2);
text-indent: -9999px;
}
}
div.unanswered {
p.status {
display: inline-block;
width: 14px;
height: 14px;
background: url('#{$static-path}/images/unanswered-icon.png') center center no-repeat;
}
}
div.correct, div.ui-icon-check {
p.status {
display: inline-block;
width: 25px;
height: 20px;
background: url('#{$static-path}/images/correct-icon.png') center center no-repeat;
}
input {
border-color: green;
}
}
div.processing {
p.status {
display: inline-block;
width: 20px;
height: 20px;
background: url('#{$static-path}/images/spinner.gif') center center no-repeat;
}
input {
border-color: #aaa;
}
}
div.incorrect, div.ui-icon-close {
p.status {
display: inline-block;
width: 20px;
height: 20px;
background: url('#{$static-path}/images/incorrect-icon.png') center center no-repeat;
text-indent: -9999px;
}
input {
border-color: red;
}
}
> span {
display: block;
margin-bottom: lh(0.5);
}
p.answer {
display: inline-block;
margin-bottom: 0;
margin-left: ($baseline/2);
&:before {
content: "Answer: ";
font-weight: bold;
display: inline;
}
&:empty {
&:before {
display: none;
}
}
}
span {
&.unanswered, &.ui-icon-bullet {
display: inline-block;
position: relative;
top: 4px;
width: 14px;
height: 14px;
background: url('#{$static-path}/images/unanswered-icon.png') center center no-repeat;
}
&.processing, &.ui-icon-processing {
display: inline-block;
position: relative;
top: 6px;
width: 25px;
height: 20px;
background: url('#{$static-path}/images/spinner.gif') center center no-repeat;
}
&.correct, &.ui-icon-check {
display: inline-block;
position: relative;
top: 6px;
width: 25px;
height: 20px;
background: url('#{$static-path}/images/correct-icon.png') center center no-repeat;
}
&.incorrect, &.ui-icon-close {
display: inline-block;
position: relative;
top: 6px;
width: 20px;
height: 20px;
background: url('#{$static-path}/images/incorrect-icon.png') center center no-repeat;
}
}
.reload {
float:right;
margin: ($baseline/2);
}
div.short-form-response {
@include clearfix();
overflow-y: auto;
margin-bottom: 0;
padding: ($baseline/2);
min-height: 20px;
height: auto;
border: 1px solid #ddd;
background: $gray-l6;
}
.grader-status {
@include clearfix();
margin: ($baseline/2) 0;
padding: ($baseline/2);
border-radius: 5px;
background: $gray-l6;
span {
display: block;
float: left;
overflow: hidden;
margin: -7px 7px 0 0;
text-indent: -9999px;
}
.grading {
margin: 0 7px 0 0;
padding-left: 25px;
background: url('#{$static-path}/images/info-icon.png') left center no-repeat;
text-indent: 0;
}
p {
float: left;
margin-bottom: 0;
line-height: 20px;
}
&.file {
margin-top: $baseline;
padding: $baseline 0 0 0;
border: 0;
border-top: 1px solid #eee;
background: $white;
p.debug {
display: none;
}
input {
float: left;
}
}
}
form.option-input {
margin: -($baseline/2) 0 $baseline;
padding-bottom: $baseline;
select {
margin-right: flex-gutter();
}
}
ul {
margin-bottom: lh();
margin-left: 0.75em;
margin-left: 0.75rem;
}
ul.rubric-list{
margin: 0;
padding: 0;
list-style-type: none;
list-style: none;
li {
&.rubric-list-item {
margin-bottom: 0;
padding: 0;
border-radius: ($baseline/4);
}
}
}
ol {
margin-bottom: lh();
margin-left: .75em;
margin-left: .75rem;
list-style: decimal outside none;
}
dl {
line-height: 1.4em;
}
dl dt {
font-weight: bold;
}
dl dd {
margin-bottom: 0;
}
dd {
margin-left: .5em;
margin-left: .5rem;
}
li {
margin-bottom: 0;
padding: 0;
&:last-child {
margin-bottom: 0;
}
}
p {
margin-bottom: lh();
}
hr {
float: none;
clear: both;
margin: 0 0 .75rem;
width: 100%;
height: 1px;
border: none;
background: #ddd;
color: #ddd;
}
.hidden {
display: none;
visibility: hidden;
}
#{$all-text-inputs} {
display: inline;
width: auto;
}
div.action {
margin-top: $baseline;
input.save {
@extend .blue-button !optional;
}
.submission_feedback {
display: inline-block;
margin: 8px 0 0 ($baseline/2);
color: #666;
font-style: italic;
-webkit-font-smoothing: antialiased;
}
}
.detailed-solution {
> p:first-child {
color: #aaa;
text-transform: uppercase;
font-weight: bold;
font-style: normal;
font-size: 0.9em;
}
p:last-child {
margin-bottom: 0;
}
}
div.open-ended-alert,
.save_message {
margin-top: ($baseline/2);
margin-bottom: ($baseline/4);
padding: 8px 12px;
border: 1px solid #ebe8bf;
border-radius: 3px;
background: #fffcdd;
font-size: 0.9em;
}
div.capa_reset {
margin-top: ($baseline/2);
margin-bottom: ($baseline/2);
padding: 25px;
border: 1px solid $error-color;
border-radius: 3px;
background-color: lighten($error-color, 25%);
font-size: 1em;
}
.capa_reset > h2 {
color: #aa0000;
}
.capa_reset li {
font-size: 0.9em;
}
.assessment-container {
margin: ($baseline*2) 0 ($baseline*1.5) 0;
.scoring-container {
p {
margin-bottom: 1em;
}
label {
display: inline-block;
margin: ($baseline/2);
padding: ($baseline/4);
min-width: 50px;
background-color: $gray-l3;
text-size: 1.5em;
}
input[type=radio]:checked + label {
background: #666;
color: white;
}
input[class='grade-selection'] {
display: none;
}
}
}
div.prompt {
background-color: white;
}
h4 {
padding: $baseline/2 0;
}
}
//OE Tool Area Styling
.oe-tools {
display: inline-block;
width: 100%;
border-radius: 5px;
.oe-tools-label, .oe-tools-scores-label {
display: inline-block;
padding: $baseline/2;
vertical-align: middle;
font-size: 0.8em;
}
.rubric-button {
padding: 8px $baseline/4;
}
.rubric-previous-button {
margin-right: $baseline/4;
}
.rubric-next-button {
margin-left: $baseline/4;
}
.next-step-button {
margin: $baseline/2;
}
.reset-button {
vertical-align: middle;
}
}
// Staff Grading
.problem-list-container {
margin: $baseline/2;
.instructions {
padding-bottom: $baseline/2;
}
}
.staff-grading {
.breadcrumbs {
padding: ($baseline/10);
background-color: $gray-l6;
border-radius: 5px;
margin-bottom: ($baseline/2);
}
.prompt-wrapper {
padding-top: ($baseline/2);
.meta-info-wrapper {
padding: ($baseline/2);
border-radius: 5px;
}
}
}
section.peer-grading-container{
div.peer-grading{
section.calibration-feedback {
padding: $baseline;
}
}
}
div.staff-info{
background-color: #eee;
border-radius: 10px;
border-bottom: 1px solid lightgray;
padding: ($baseline/2);
margin: ($baseline/2) 0 ($baseline/2) 0;
}

View File

@@ -1,105 +0,0 @@
.editor-bar {
.editor-tabs {
.advanced-toggle {
@include white-button;
height: auto;
margin-top: -1px;
padding: 3px 9px;
font-size: 12px;
&.current {
border: 1px solid $lightGrey !important;
border-radius: 3px !important;
background: $lightGrey !important;
color: $darkGrey !important;
pointer-events: none;
cursor: none;
&:hover, &:focus {
box-shadow: 0 0 0 0 !important;
}
}
}
.cheatsheet-toggle {
width: 21px;
height: 21px;
padding: 0;
margin: 0 ($baseline/4) 0 ($baseline*0.75);
border-radius: 22px;
border: 1px solid #a5aaaf;
background: #e5ecf3;
font-size: 13px;
font-weight: 700;
color: #565d64;
text-align: center;
}
}
}
.simple-editor-open-ended-cheatsheet {
position: absolute;
top: 0;
left: 100%;
width: 0;
border-radius: 0 3px 3px 0;
@include linear-gradient(left, $shadow-l1, $transparent 4px);
background-color: $white;
overflow: hidden;
@include transition(width .3s linear 0s);
&.shown {
width: 20%;
height: 100%;
overflow-y: scroll;
}
.cheatsheet-wrapper {
padding: 10%;
}
h6 {
margin-bottom: 7px;
font-size: 15px;
font-weight: 700;
}
.row {
@include clearfix();
padding-bottom: 5px !important;
margin-bottom: 10px !important;
border-bottom: 1px solid #ddd !important;
&:last-child {
border-bottom: none !important;
margin-bottom: 0 !important;
}
}
.col {
float: left;
&.sample {
width: 60px;
margin-right: 30px;
}
}
pre {
font-size: 12px;
line-height: 18px;
}
code {
padding: 0;
background: none;
}
}
.combinedopenended-editor-icon {
display: inline-block;
vertical-align: middle;
color: #333;
}

View File

@@ -1,20 +0,0 @@
$leaderboard: #F4F4F4;
section.foldit {
div.folditchallenge {
table {
border: 1px solid lighten($leaderboard, 10%);
border-collapse: collapse;
margin-top: $baseline;
}
th {
background: $leaderboard;
color: darken($leaderboard, 25%);
}
td {
background: lighten($leaderboard, 3%);
border-bottom: 1px solid $white;
padding: 8px;
}
}
}

View File

@@ -91,7 +91,7 @@ ul {
a {
&:link, &:visited, &:hover, &:active, &:focus {
color: #1d9dd9;
color: $blue;
}
}

View File

@@ -1,206 +0,0 @@
import logging
from lxml import etree
from pkg_resources import resource_string
from xmodule.editing_module import EditingDescriptor
from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor
from xblock.fields import Scope, Integer, String
from .fields import Date
log = logging.getLogger(__name__)
class FolditFields(object):
# default to what Spring_7012x uses
required_level_half_credit = Integer(default=3, scope=Scope.settings)
required_sublevel_half_credit = Integer(default=5, scope=Scope.settings)
required_level = Integer(default=4, scope=Scope.settings)
required_sublevel = Integer(default=5, scope=Scope.settings)
due = Date(help="Date that this problem is due by", scope=Scope.settings)
show_basic_score = String(scope=Scope.settings, default='false')
show_leaderboard = String(scope=Scope.settings, default='false')
class FolditModule(FolditFields, XModule):
css = {'scss': [resource_string(__name__, 'css/foldit/leaderboard.scss')]}
def __init__(self, *args, **kwargs):
"""
Example:
<foldit show_basic_score="true"
required_level="4"
required_sublevel="3"
required_level_half_credit="2"
required_sublevel_half_credit="3"
show_leaderboard="false"/>
"""
super(FolditModule, self).__init__(*args, **kwargs)
self.due_time = self.due
def is_complete(self):
"""
Did the user get to the required level before the due date?
"""
# We normally don't want django dependencies in xmodule. foldit is
# special. Import this late to avoid errors with things not yet being
# initialized.
from foldit.models import PuzzleComplete
complete = PuzzleComplete.is_level_complete(
self.system.anonymous_student_id,
self.required_level,
self.required_sublevel,
self.due_time)
return complete
def is_half_complete(self):
"""
Did the user reach the required level for half credit?
Ideally this would be more flexible than just 0, 0.5, or 1 credit. On
the other hand, the xml attributes for specifying more specific
cut-offs and partial grades can get more confusing.
"""
from foldit.models import PuzzleComplete
complete = PuzzleComplete.is_level_complete(
self.system.anonymous_student_id,
self.required_level_half_credit,
self.required_sublevel_half_credit,
self.due_time)
return complete
def completed_puzzles(self):
"""
Return a list of puzzles that this user has completed, as an array of
dicts:
[ {'set': int,
'subset': int,
'created': datetime} ]
The list is sorted by set, then subset
"""
from foldit.models import PuzzleComplete
return sorted(
PuzzleComplete.completed_puzzles(self.system.anonymous_student_id),
key=lambda d: (d['set'], d['subset']))
def puzzle_leaders(self, n=10, courses=None):
"""
Returns a list of n pairs (user, score) corresponding to the top
scores; the pairs are in descending order of score.
"""
from foldit.models import Score
if courses is None:
courses = [self.location.course_key]
leaders = [(leader['username'], leader['score']) for leader in Score.get_tops_n(10, course_list=courses)]
leaders.sort(key=lambda x: -x[1])
return leaders
def get_html(self):
"""
Render the html for the module.
"""
goal_level = '{0}-{1}'.format(
self.required_level,
self.required_sublevel)
showbasic = (self.show_basic_score.lower() == "true")
showleader = (self.show_leaderboard.lower() == "true")
context = {
'due': self.due,
'success': self.is_complete(),
'goal_level': goal_level,
'completed': self.completed_puzzles(),
'top_scores': self.puzzle_leaders(),
'show_basic': showbasic,
'show_leader': showleader,
'folditbasic': self.get_basicpuzzles_html(),
'folditchallenge': self.get_challenge_html()
}
return self.system.render_template('foldit.html', context)
def get_basicpuzzles_html(self):
"""
Render html for the basic puzzle section.
"""
goal_level = '{0}-{1}'.format(
self.required_level,
self.required_sublevel)
context = {
'due': self.due,
'success': self.is_complete(),
'goal_level': goal_level,
'completed': self.completed_puzzles(),
}
return self.system.render_template('folditbasic.html', context)
def get_challenge_html(self):
"""
Render html for challenge (i.e., the leaderboard)
"""
context = {
'top_scores': self.puzzle_leaders()}
return self.system.render_template('folditchallenge.html', context)
def get_score(self):
"""
0 if required_level_half_credit - required_sublevel_half_credit not
reached.
0.5 if required_level_half_credit and required_sublevel_half_credit
reached.
1 if requred_level and required_sublevel reached.
"""
if self.is_complete():
score = 1
elif self.is_half_complete():
score = 0.5
else:
score = 0
return {'score': score,
'total': self.max_score()}
def max_score(self):
return 1
class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
"""
Module for adding Foldit problems to courses
"""
mako_template = "widgets/html-edit.html"
module_class = FolditModule
filename_extension = "xml"
has_score = True
show_in_read_only_mode = True
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor"
# The grade changes without any student interaction with the edx website,
# so always need to actually check.
always_recalculate_grades = True
@classmethod
def definition_from_xml(cls, xml_object, system):
return {}, []
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('foldit')
return xml_object

View File

@@ -1,128 +0,0 @@
<section class="course-content">
<section class="xblock xblock-student_view xmodule_display xmodule_CombinedOpenEndedModule" data-type="CombinedOpenEnded">
<section id="combined-open-ended" class="combined-open-ended" data-ajax-url="/courses/MITx/6.002x/2012_Fall/modx/i4x://MITx/6.002x/combinedopenended/CombinedOE" data-allow_reset="False" data-state="assessing" data-task-count="2" data-task-number="1">
<h2>Problem 1</h2>
<div class="status-container">
<h4>Status</h4>
<div class="status-elements">
<section id="combined-open-ended-status" class="combined-open-ended-status">
<div class="statusitem" data-status-number="0">
Step 1 (Problem complete) : 1 / 1
<span class="correct" id="status"></span>
</div>
<div class="statusitem statusitem-current" data-status-number="1">
Step 2 (Being scored) : None / 1
<span class="grading" id="status"></span>
</div>
</section>
</div>
</div>
<div class="item-container">
<h4>Problem</h4>
<div class="problem-container">
<div class="item">
<section id="openended_open_ended" class="open-ended-child" data-state="assessing" data-child-type="openended">
<div class="error">
</div>
<div class="prompt">
Some prompt.
</div>
<textarea rows="30" cols="80" name="answer" class="answer short-form-response" id="input_open_ended" disabled="disabled">
Test submission. Yaaaaaaay!
</textarea>
<div class="message-wrapper"></div>
<div class="grader-status">
<span class="grading" id="status_open_ended">Submitted for grading.</span>
</div>
<input type="button" value="Submit assessment" class="submit-button" name="show" style="display: none;">
<input name="skip" class="skip-button" type="button" value="Skip Post-Assessment" style="display: none;">
<div class="open-ended-action"></div>
<span id="answer_open_ended"></span>
</section>
</div>
</div>
<div class="oe-tools response-tools">
<span class="oe-tools-label"></span>
<input type="button" value="Reset" class="reset-button" name="reset" style="display: none;">
</div>
<input type="button" value="Next Step" class="next-step-button" name="reset" style="display: none;">
</div>
<a name="results">
<div class="result-container">
</div>
</a>
</section>
<a name="results">
</a>
</section>
<a name="results">
</a>
<div>
<a name="results">
</a>
<a href="https://github.com/MITx/content-mit-6002x/tree/master/combinedopenended/CombinedOE.xml">
Edit
</a> /
<a href="#i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa-modal" onclick="javascript:getlog('i4x_MITx_6_002x_combinedopenended_CombinedOE', {
'location': 'i4x://MITx/6.002x/combinedopenended/CombinedOE',
'xqa_key': 'KUBrWtK3RAaBALLbccHrXeD3RHOpmZ2A',
'category': 'CombinedOpenEndedModule',
'user': 'blah'
})" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_log">QA</a>
</div>
<div>
<a href="#i4x_MITx_6_002x_combinedopenended_CombinedOE_debug" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_trig">
Staff Debug Info
</a>
</div>
<section id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa-modal" class="modal xqa-modal" style="width:80%; left:20%; height:80%; overflow:auto">
<div class="inner-wrapper">
<header>
<h2>edX Content Quality Assessment</h2>
</header>
<form id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_form" class="xqa_form">
<label>Comment</label>
<input id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_entry" type="text" placeholder="comment">
<label>Tag</label>
<span style="color:black;vertical-align: -10pt">Optional tag (eg "done" or "broken"):&nbsp; </span>
<input id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_tag" type="text" placeholder="tag" style="width:80px;display:inline">
<div class="submit">
<button name="submit" type="submit">Add comment</button>
</div>
<hr>
<div id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_log_data"></div>
</form>
</div>
</section>
<section class="modal staff-modal" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_debug" style="width:80%; left:20%; height:80%; overflow:auto;">
<div class="inner-wrapper" style="color:black">
<header>
<h2>Staff Debug</h2>
</header>
<div class="staff_info" style="display:block">
is_released = <font color="red">Yes!</font>
location = i4x://MITx/6.002x/combinedopenended/CombinedOE
github = <a href="https://github.com/MITx/content-mit-6002x/tree/master/combinedopenended/CombinedOE.xml">https://github.com/MITx/content-mit-6002x/tree/master/combinedopenended/CombinedOE.xml</a>
definition = <pre>None</pre>
metadata = {
"showanswer": "attempted",
"display_name": "Problem 1",
"graceperiod": "1 day 12 hours 59 minutes 59 seconds",
"xqa_key": "KUBrWtK3RAaBALLbccHrXeD3RHOpmZ2A",
"rerandomize": "never",
"start": "2012-09-05T12:00",
"attempts": "10000",
"data_dir": "content-mit-6002x",
"max_score": "1"
}
category = CombinedOpenEndedModule
</div>
</div>
</section>
<div id="i4x_MITx_6_002x_combinedopenended_CombinedOE_setup"></div>
</section>

View File

@@ -1,6 +0,0 @@
<section class="combinedopenended-editor editor">
<div class="row">
<textarea class="markdown-box">markdown</textarea>
<textarea class="xml-box" rows="8" cols="40">xml</textarea>
</div>
</section>

View File

@@ -1,5 +0,0 @@
<section class="combinedopenended-editor editor">
<div class="row">
<textarea class="xml-box" rows="8" cols="40">xml only</textarea>
</div>
</section>

View File

@@ -1,321 +0,0 @@
<section id="combined-open-ended" class="combined-open-ended" data-location="i4x://test/2323/combinedopenended/b893eedec151441f8644187266ccce00" data-ajax-url="/courses/test/2323/Test2/modx/i4x://test/2323/combinedopenended/b893eedec151441f8644187266ccce00" data-allow_reset="False" data-state="initial" data-task-count="1" data-task-number="1" data-accept-file-upload="False">
<div class="name">
<h2>Open Response Assessment</h2>
<div class="progress-container">
</div>
</div>
<div class="problemwrapper">
<div class="status-bar">
<table class="statustable">
<tbody><tr>
<td class="problemtype-container">
<div class="problemtype">
Open Response
</div>
</td>
<td class="assessments-container">
<div class="assessment-text">
Assessments:
</div>
<div class="status-container">
<div class="status-elements">
<section id="combined-open-ended-status" class="combined-open-ended-status">
<div class="statusitem statusitem-current" data-status-number="0">
Peer
</div>
</section>
</div>
</div>
</td>
</tr>
</tbody></table>
</div>
<div class="item-container">
<div class="visibility-control visibility-control-prompt">
<div class="inner">
</div>
<a href="" class="question-header">Show Question</a>
</div>
<div class="problem-container">
<div class="item">
<section id="openended_open_ended" class="open-ended-child" data-state="post_assessment" data-child-type="openended">
<div class="error"></div>
<div class="prompt open" style="display: none;">
<h3>Censorship in the Libraries</h3><p>'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author
</p><p>
Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.
</p>
</div>
<div class="visibility-control visibility-control-response">
<div class="inner">
</div>
<span class="section-header section-header-response">Response</span>
</div>
<div class="answer short-form-response" id="input_open_ended"></div>
<div class="message-wrapper"></div>
<div class="grader-status">
</div>
<div class="file-upload"></div>
<input type="button" value="Submit post-assessment" class="submit-button" name="show" style="display: none;">
<input name="skip" class="skip-button" type="button" value="Skip Post-Assessment" style="display: none;">
<div class="open-ended-action"></div>
<span id="answer_open_ended"></span>
</section>
</div>
</div>
<div class="oe-tools response-tools">
<span class="oe-tools-label"></span>
<input type="button" value="Reset" class="reset-button" name="reset" style="display: inline-block;">
</div>
</div>
<div class="combined-rubric-container" data-status="shown" data-number="0" style="">
<div class="visibility-control visibility-control-rubric">
<div class="inner">
</div>
<span class="section-header section-header-rubric">Submitted Rubric</span>
</div>
<div class="rubric-header">
<button class="rubric-collapse" href="#">Show Full Rubric</button>
Scored rubric from grader 1
</div>
<div class="rubric">
<span class="rubric-category">
Ideas
</span> <br>
<ul class="rubric-list">
<li class="rubric-list-item">
<div class="rubric-label">
<label class="choicegroup_incorrect wrapper-score-selection"></label>
<span class="wrappable"> 0 points :
Difficult for the reader to discern the main idea. Too brief or too repetitive to establish or maintain a focus.
</span>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 1 points :
Attempts a main idea. Sometimes loses focus or ineffectively displays focus.
</span>
</label>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 2 points :
Presents a unifying theme or main idea, but may include minor tangents. Stays somewhat focused on topic and task.
</span>
</label>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 3 points :
Presents a unifying theme or main idea without going off on tangents. Stays completely focused on topic and task.
</span>
</label>
</div>
</li>
</ul>
<span class="rubric-category">
Content
</span> <br>
<ul class="rubric-list">
<li class="rubric-list-item">
<div class="rubric-label">
<label class="choicegroup_incorrect wrapper-score-selection"></label>
<span class="wrappable"> 0 points :
Includes little information with few or no details or unrelated details. Unsuccessful in attempts to explore any facets of the topic.
</span>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 1 points :
Includes little information and few or no details. Explores only one or two facets of the topic.
</span>
</label>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 2 points :
Includes sufficient information and supporting details. (Details may not be fully developed; ideas may be listed.) Explores some facets of the topic.
</span>
</label>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 3 points :
Includes in-depth information and exceptional supporting details that are fully developed. Explores all facets of the topic.
</span>
</label>
</div>
</li>
</ul>
<span class="rubric-category">
Organization
</span> <br>
<ul class="rubric-list">
<li class="rubric-list-item">
<div class="rubric-label">
<label class="choicegroup_incorrect wrapper-score-selection"></label>
<span class="wrappable"> 0 points :
Ideas organized illogically, transitions weak, and response difficult to follow.
</span>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 1 points :
Attempts to logically organize ideas. Attempts to progress in an order that enhances meaning, and demonstrates use of transitions.
</span>
</label>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 2 points :
Ideas organized logically. Progresses in an order that enhances meaning. Includes smooth transitions.
</span>
</label>
</div>
</li>
</ul>
<span class="rubric-category">
Style
</span> <br>
<ul class="rubric-list">
<li class="rubric-list-item">
<div class="rubric-label">
<label class="choicegroup_incorrect wrapper-score-selection"></label>
<span class="wrappable"> 0 points :
Contains limited vocabulary, with many words used incorrectly. Demonstrates problems with sentence patterns.
</span>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 1 points :
Contains basic vocabulary, with words that are predictable and common. Contains mostly simple sentences (although there may be an attempt at more varied sentence patterns).
</span>
</label>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 2 points :
Includes vocabulary to make explanations detailed and precise. Includes varied sentence patterns, including complex sentences.
</span>
</label>
</div>
</li>
</ul>
<span class="rubric-category">
Voice
</span> <br>
<ul class="rubric-list">
<li class="rubric-list-item">
<div class="rubric-label">
<label class="choicegroup_incorrect wrapper-score-selection"></label>
<span class="wrappable"> 0 points :
Demonstrates language and tone that may be inappropriate to task and reader.
</span>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 1 points :
Demonstrates an attempt to adjust language and tone to task and reader.
</span>
</label>
</div>
</li>
<li class="rubric-list-item rubric-info-item" style="display: none;">
<div class="rubric-label">
<label class="rubric-elements-info">
<span class="wrapper-score-selection"> </span>
<span class="wrappable"> 2 points :
Demonstrates effective adjustment of language and tone to task and reader.
</span>
</label>
</div>
</li>
</ul>
</div>
<div class="written-feedback">
</div>
</div>
<input type="button" value="Next Step" class="next-step-button" name="reset" style="display: none;">
<section class="legend-container">
</section>
<div class="result-container">
</div>
</div>
</section>

Some files were not shown because too many files have changed in this diff Show More