Merge branch 'master' into feature/christina/metadata
This commit is contained in:
@@ -11,6 +11,7 @@ Feature: Advanced (manual) course policy
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
Then the settings are alphabetized
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Test cancel editing key value
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I edit the value of a policy key
|
||||
@@ -19,6 +20,7 @@ Feature: Advanced (manual) course policy
|
||||
And I reload the page
|
||||
Then the policy key value is unchanged
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Test editing key value
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I edit the value of a policy key and save
|
||||
@@ -26,6 +28,7 @@ Feature: Advanced (manual) course policy
|
||||
And I reload the page
|
||||
Then the policy key value is changed
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Test how multi-line input appears
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a JSON object as a value
|
||||
@@ -33,6 +36,7 @@ Feature: Advanced (manual) course policy
|
||||
And I reload the page
|
||||
Then it is displayed as formatted
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Test automatic quoting of non-JSON values
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a non-JSON value not in quotes
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
import time
|
||||
from terrain.steps import reload_the_page
|
||||
|
||||
from nose.tools import assert_true, assert_false, assert_equal
|
||||
from nose.tools import assert_false, assert_equal
|
||||
|
||||
"""
|
||||
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
|
||||
@@ -18,8 +15,8 @@ VALUE_CSS = 'textarea.json'
|
||||
DISPLAY_NAME_KEY = "display_name"
|
||||
DISPLAY_NAME_VALUE = '"Robot Super Course"'
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
############### ACTIONS ####################
|
||||
@step('I select the Advanced Settings$')
|
||||
def i_select_advanced_settings(step):
|
||||
expand_icon_css = 'li.nav-course-settings i.icon-expand'
|
||||
@@ -38,7 +35,7 @@ def i_am_on_advanced_course_settings(step):
|
||||
@step(u'I press the "([^"]*)" notification button$')
|
||||
def press_the_notification_button(step, name):
|
||||
css = 'a.%s-button' % name.lower()
|
||||
world.css_click_at(css)
|
||||
world.css_click(css)
|
||||
|
||||
|
||||
@step(u'I edit the value of a policy key$')
|
||||
@@ -52,7 +49,7 @@ def edit_the_value_of_a_policy_key(step):
|
||||
|
||||
|
||||
@step(u'I edit the value of a policy key and save$')
|
||||
def edit_the_value_of_a_policy_key(step):
|
||||
def edit_the_value_of_a_policy_key_and_save(step):
|
||||
change_display_name_value(step, '"foo"')
|
||||
|
||||
|
||||
@@ -90,7 +87,7 @@ def it_is_formatted(step):
|
||||
|
||||
|
||||
@step('it is displayed as a string')
|
||||
def it_is_formatted(step):
|
||||
def it_is_displayed_as_string(step):
|
||||
assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"'])
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ Feature: Course checklists
|
||||
Then I can check and uncheck tasks in a checklist
|
||||
And They are correctly selected after I reload the page
|
||||
|
||||
@skip-phantom
|
||||
@skip-firefox
|
||||
Scenario: A task can link to a location within Studio
|
||||
Given I have opened Checklists
|
||||
When I select a link to the course outline
|
||||
@@ -17,8 +19,9 @@ Feature: Course checklists
|
||||
And I press the browser back button
|
||||
Then I am brought back to the course outline in the correct state
|
||||
|
||||
@skip-phantom
|
||||
@skip-firefox
|
||||
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
|
||||
|
||||
|
||||
@@ -89,8 +89,6 @@ def i_am_brought_to_help_page_in_new_window(step):
|
||||
assert_equal('http://help.edge.edx.org/', world.browser.url)
|
||||
|
||||
|
||||
|
||||
|
||||
############### HELPER METHODS ####################
|
||||
def verifyChecklist2Status(completed, total, percentage):
|
||||
def verify_count(driver):
|
||||
@@ -107,9 +105,11 @@ def verifyChecklist2Status(completed, total, percentage):
|
||||
|
||||
|
||||
def toggleTask(checklist, task):
|
||||
world.css_click('#course-checklist' + str(checklist) +'-task' + str(task))
|
||||
world.css_click('#course-checklist' + str(checklist) + '-task' + str(task))
|
||||
|
||||
|
||||
# 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):
|
||||
# toggle checklist item to make sure that the link button is showing
|
||||
toggleTask(checklist, task)
|
||||
@@ -121,4 +121,3 @@ def clickActionLink(checklist, task, actionText):
|
||||
|
||||
world.wait_for(verify_action_link_text)
|
||||
action_link.click()
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
Feature: Course Settings
|
||||
As a course author, I want to be able to configure my course settings.
|
||||
|
||||
@skip-phantom
|
||||
Scenario: User can set course dates
|
||||
Given I have opened a new course in Studio
|
||||
When I select Schedule and Details
|
||||
And I set course dates
|
||||
Then I see the set dates on refresh
|
||||
|
||||
@skip-phantom
|
||||
Scenario: User can clear previously set course dates (except start date)
|
||||
Given I have set course dates
|
||||
And I clear all the dates except start
|
||||
Then I see cleared dates on refresh
|
||||
|
||||
@skip-phantom
|
||||
Scenario: User cannot clear the course start date
|
||||
Given I have set course dates
|
||||
And I clear the course start date
|
||||
|
||||
@@ -3,6 +3,7 @@ Feature: Create Section
|
||||
As a course author
|
||||
I want to create and edit sections
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Add a new section to a course
|
||||
Given I have opened a new course in Studio
|
||||
When I click the New Section link
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
Feature: Overview Toggle Section
|
||||
In order to quickly view the details of a course's section or to scan the inventory of sections
|
||||
As a course author
|
||||
I want to toggle the visibility of each section's subsection details in the overview listing
|
||||
As a course author
|
||||
I want to toggle the visibility of each section's subsection details in the overview listing
|
||||
|
||||
Scenario: The default layout for the overview page is to show sections in expanded view
|
||||
Given I have a course with multiple sections
|
||||
When I navigate to the course overview page
|
||||
Then I see the "Collapse All Sections" link
|
||||
And all sections are expanded
|
||||
When I navigate to the course overview page
|
||||
Then I see the "Collapse All Sections" link
|
||||
And all sections are expanded
|
||||
|
||||
Scenario: Expand /collapse for a course with no sections
|
||||
Given I have a course with no sections
|
||||
When I navigate to the course overview page
|
||||
Then I do not see the "Collapse All Sections" link
|
||||
When I navigate to the course overview page
|
||||
Then I do not see the "Collapse All Sections" link
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Collapse link appears after creating first section of a course
|
||||
Given I have a course with no sections
|
||||
When I navigate to the course overview page
|
||||
And I add a section
|
||||
Then I see the "Collapse All Sections" link
|
||||
And all sections are expanded
|
||||
When I navigate to the course overview page
|
||||
And I add a section
|
||||
Then I see the "Collapse All Sections" link
|
||||
And all sections are expanded
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Collapse link is not removed after last section of a course is deleted
|
||||
Given I have a course with 1 section
|
||||
And I navigate to the course overview page
|
||||
When I press the "section" delete icon
|
||||
And I confirm the alert
|
||||
And I navigate to the course overview page
|
||||
When I press the "section" delete icon
|
||||
And I confirm the alert
|
||||
Then I see the "Collapse All Sections" link
|
||||
|
||||
Scenario: Collapsing all sections when all sections are expanded
|
||||
@@ -57,4 +58,4 @@ Feature: Overview Toggle Section
|
||||
When I expand the first section
|
||||
And I click the "Expand All Sections" link
|
||||
Then I see the "Collapse All Sections" link
|
||||
And all sections are expanded
|
||||
And all sections are expanded
|
||||
|
||||
@@ -3,13 +3,15 @@ Feature: Create Subsection
|
||||
As a course author
|
||||
I want to create and edit subsections
|
||||
|
||||
Scenario: Add a new subsection to a section
|
||||
@skip-phantom
|
||||
Scenario: Add a new subsection to a section
|
||||
Given I have opened a new course section in Studio
|
||||
When I click the New Subsection link
|
||||
And I enter the subsection name and click save
|
||||
Then I see my subsection on the Courseware page
|
||||
|
||||
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
|
||||
@skip-phantom
|
||||
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
|
||||
Given I have opened a new course section in Studio
|
||||
When I click the New Subsection link
|
||||
And I enter a subsection name with a quote and click save
|
||||
@@ -17,7 +19,7 @@ Feature: Create Subsection
|
||||
And I click to edit the subsection name
|
||||
Then I see the complete subsection name with a quote in the editor
|
||||
|
||||
Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258)
|
||||
Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258)
|
||||
Given I have opened a new course section in Studio
|
||||
And I have added a new subsection
|
||||
And I mark it as Homework
|
||||
@@ -25,20 +27,19 @@ Feature: Create Subsection
|
||||
And I reload the page
|
||||
Then I see it marked as Homework
|
||||
|
||||
Scenario: Set a due date in a different year (bug #256)
|
||||
@skip-phantom
|
||||
Scenario: Set a due date in a different year (bug #256)
|
||||
Given I have opened a new subsection in Studio
|
||||
And I have set a release date and due date in different years
|
||||
Then I see the correct dates
|
||||
And I reload the page
|
||||
Then I see the correct dates
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Delete a subsection
|
||||
@skip-phantom
|
||||
Scenario: Delete a subsection
|
||||
Given I have opened a new course section in Studio
|
||||
And I have added a new subsection
|
||||
And I see my subsection on the Courseware page
|
||||
When I press the "subsection" delete icon
|
||||
And I confirm the alert
|
||||
Then the subsection does not exist
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from tempdir import mkdtemp_clean
|
||||
from fs.osfs import OSFS
|
||||
import copy
|
||||
from json import loads
|
||||
import traceback
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
@@ -397,7 +396,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
|
||||
draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'no_references', 'draft']))
|
||||
'vertical', 'no_references', 'draft']))
|
||||
|
||||
for child in vertical.get_children():
|
||||
draft_store.clone_item(child.location, child.location)
|
||||
@@ -478,6 +477,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
for child in vertical.get_children():
|
||||
self.assertTrue(getattr(child, 'is_draft', False))
|
||||
|
||||
# make sure that we don't have a sequential that is in draft mode
|
||||
sequential = draft_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
|
||||
self.assertFalse(getattr(sequential, 'is_draft', False))
|
||||
|
||||
# verify that we have the private vertical
|
||||
test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'vertical_66', None]))
|
||||
|
||||
@@ -36,3 +36,4 @@ DATABASES = {
|
||||
INSTALLED_APPS += ('lettuce.django',)
|
||||
LETTUCE_APPS = ('contentstore',)
|
||||
LETTUCE_SERVER_PORT = 8001
|
||||
LETTUCE_BROWSER = 'chrome'
|
||||
|
||||
@@ -87,12 +87,12 @@ from contentstore import utils
|
||||
<div class="note note-promotion note-promotion-courseURL has-actions">
|
||||
<h3 class="title">Course Summary Page <span class="tip">(for student enrollment and access)</span></h3>
|
||||
<div class="copy">
|
||||
<p><a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />${utils.get_lms_link_for_about_page(course_location)}</a></p>
|
||||
<p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" />https:${utils.get_lms_link_for_about_page(course_location)}</a></p>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<a title="Send a note to students via email" href="mailto:john.doe@gmail.com?Subject=Enroll%20in%20COURSENAME&body=Hi,%20COURSENAME,%20provided%20by%20edX,%20is%20almost%20ready%20to%20begin.%20Please%20enroll%20for%20this%20course%20at%20${utils.get_lms_link_for_about_page(course_location)}." class="action action-primary"><i class="ss-icon icon ss-symbolicons-standard icon icon-inline icon-announcement">✉</i> Send an invitation to your students</a>
|
||||
<a title="Send a note to students via email" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20"${context_course.display_name_with_default}",%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="ss-icon icon ss-symbolicons-standard icon icon-inline icon-announcement">✉</i> Invite your students</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from contentstore import utils
|
||||
from contentstore import utils
|
||||
%>
|
||||
|
||||
<%block name="jsextra">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
|
||||
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/settings/settings_grading_view.js')}"></script>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
|
||||
@@ -26,15 +26,15 @@ from contentstore import utils
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
|
||||
|
||||
var editor = new CMS.Views.Settings.Grading({
|
||||
el: $('.settings-grading'),
|
||||
model : new CMS.Models.Settings.CourseGradingPolicy(${course_details|n},{parse:true})
|
||||
});
|
||||
|
||||
|
||||
editor.render();
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -97,7 +97,7 @@ from contentstore import utils
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-grading-graceperiod">
|
||||
<label for="course-grading-graceperiod">Grace Period on Deadline:</label>
|
||||
<input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="e.g. 10 minutes">
|
||||
<input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-inline">Leeway on due dates</span>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -112,13 +112,13 @@ from contentstore import utils
|
||||
</header>
|
||||
|
||||
<ol class="list-input course-grading-assignment-list enum">
|
||||
|
||||
</ol>
|
||||
|
||||
</ol>
|
||||
|
||||
<div class="actions">
|
||||
<a href="#" class="new-button new-course-grading-item add-grading-data">
|
||||
<span class="plus-icon white"></span>New Assignment Type
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
@@ -2,17 +2,17 @@ from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed, CourseEnrollment)
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
from factory import Factory, SubFactory, post_generation
|
||||
from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class GroupFactory(Factory):
|
||||
class GroupFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = Group
|
||||
|
||||
name = 'staff_MITx/999/Robot_Super_Course'
|
||||
|
||||
|
||||
class UserProfileFactory(Factory):
|
||||
class UserProfileFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
@@ -23,19 +23,20 @@ class UserProfileFactory(Factory):
|
||||
goals = 'World domination'
|
||||
|
||||
|
||||
class RegistrationFactory(Factory):
|
||||
class RegistrationFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid4().hex
|
||||
|
||||
|
||||
class UserFactory(Factory):
|
||||
class UserFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
email = 'robot+test@edx.org'
|
||||
password = 'test'
|
||||
password = PostGenerationMethodCall('set_password',
|
||||
'test')
|
||||
first_name = 'Robot'
|
||||
last_name = 'Test'
|
||||
is_staff = False
|
||||
@@ -44,26 +45,19 @@ class UserFactory(Factory):
|
||||
last_login = datetime(2012, 1, 1)
|
||||
date_joined = datetime(2011, 1, 1)
|
||||
|
||||
@post_generation
|
||||
def set_password(self, create, extracted, **kwargs):
|
||||
self._raw_password = self.password
|
||||
self.set_password(self.password)
|
||||
if create:
|
||||
self.save()
|
||||
|
||||
|
||||
class AdminFactory(UserFactory):
|
||||
is_staff = True
|
||||
|
||||
|
||||
class CourseEnrollmentFactory(Factory):
|
||||
class CourseEnrollmentFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = CourseEnrollment
|
||||
|
||||
user = SubFactory(UserFactory)
|
||||
course_id = 'edX/toy/2012_Fall'
|
||||
|
||||
|
||||
class CourseEnrollmentAllowedFactory(Factory):
|
||||
class CourseEnrollmentAllowedFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = CourseEnrollmentAllowed
|
||||
|
||||
email = 'test@edx.org'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from lettuce import before, after, world
|
||||
from splinter.browser import Browser
|
||||
from logging import getLogger
|
||||
from django.core.management import call_command
|
||||
from django.conf import settings
|
||||
|
||||
# Let the LMS and CMS do their one-time setup
|
||||
# For example, setting up mongo caches
|
||||
@@ -10,18 +12,14 @@ from cms import one_time_startup
|
||||
logger = getLogger(__name__)
|
||||
logger.info("Loading the lettuce acceptance testing terrain file...")
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
|
||||
@before.harvest
|
||||
def initial_setup(server):
|
||||
'''
|
||||
Launch the browser once before executing the tests
|
||||
'''
|
||||
# Launch the browser app (choose one of these below)
|
||||
world.browser = Browser('chrome')
|
||||
# world.browser = Browser('phantomjs')
|
||||
# world.browser = Browser('firefox')
|
||||
browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
|
||||
world.browser = Browser(browser_driver)
|
||||
|
||||
|
||||
@before.each_scenario
|
||||
@@ -34,6 +32,15 @@ def reset_data(scenario):
|
||||
call_command('flush', interactive=False)
|
||||
|
||||
|
||||
@after.each_scenario
|
||||
def screenshot_on_error(scenario):
|
||||
'''
|
||||
Save a screenshot to help with debugging
|
||||
'''
|
||||
if scenario.failed:
|
||||
world.browser.driver.save_screenshot('/tmp/last_failed_scenario.png')
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_browser(total):
|
||||
'''
|
||||
|
||||
@@ -132,6 +132,8 @@ def i_am_logged_in(step):
|
||||
world.create_user('robot')
|
||||
world.log_in('robot', 'test')
|
||||
world.browser.visit(django_url('/'))
|
||||
# You should not see the login link
|
||||
assert_equals(world.browser.find_by_css('a#login'), [])
|
||||
|
||||
|
||||
@step(u'I am an edX user$')
|
||||
|
||||
@@ -105,8 +105,12 @@ def add_histogram(get_html, module, user):
|
||||
return get_html()
|
||||
|
||||
module_id = module.id
|
||||
histogram = grade_histogram(module_id)
|
||||
render_histogram = len(histogram) > 0
|
||||
if module.descriptor.has_score:
|
||||
histogram = grade_histogram(module_id)
|
||||
render_histogram = len(histogram) > 0
|
||||
else:
|
||||
histogram = None
|
||||
render_histogram = False
|
||||
|
||||
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
|
||||
[filepath, filename] = getattr(module.descriptor, 'xml_attributes', {}).get('filename', ['', None])
|
||||
|
||||
@@ -668,6 +668,8 @@ class MatlabInput(CodeInput):
|
||||
# Check if problem has been queued
|
||||
self.queuename = 'matlab'
|
||||
self.queue_msg = ''
|
||||
# this is only set if we don't have a graded response
|
||||
# the graded response takes precedence
|
||||
if 'queue_msg' in self.input_state and self.status in ['queued', 'incomplete', 'unsubmitted']:
|
||||
self.queue_msg = self.input_state['queue_msg']
|
||||
if 'queuestate' in self.input_state and self.input_state['queuestate'] == 'queued':
|
||||
@@ -712,11 +714,23 @@ class MatlabInput(CodeInput):
|
||||
self.input_state['queuestate'] = None
|
||||
self.input_state['queuekey'] = None
|
||||
|
||||
def button_enabled(self):
|
||||
""" Return whether or not we want the 'Test Code' button visible
|
||||
|
||||
Right now, we only want this button to show up when a problem has not been
|
||||
checked.
|
||||
"""
|
||||
if self.status in ['correct', 'incorrect']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _extra_context(self):
|
||||
''' Set up additional context variables'''
|
||||
extra_context = {
|
||||
'queue_len': str(self.queue_len),
|
||||
'queue_msg': self.queue_msg
|
||||
'queue_msg': self.queue_msg,
|
||||
'button_enabled': self.button_enabled(),
|
||||
}
|
||||
return extra_context
|
||||
|
||||
@@ -766,10 +780,6 @@ class MatlabInput(CodeInput):
|
||||
lms_key=queuekey,
|
||||
queue_name=self.queuename)
|
||||
|
||||
# save the input state
|
||||
self.input_state['queuekey'] = queuekey
|
||||
self.input_state['queuestate'] = 'queued'
|
||||
|
||||
# construct xqueue body
|
||||
student_info = {'anonymous_student_id': anonymous_student_id,
|
||||
'submission_time': qtime}
|
||||
@@ -779,6 +789,10 @@ class MatlabInput(CodeInput):
|
||||
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body=json.dumps(contents))
|
||||
# save the input state if successful
|
||||
if error == 0:
|
||||
self.input_state['queuekey'] = queuekey
|
||||
self.input_state['queuestate'] = 'queued'
|
||||
|
||||
return {'success': error == 0, 'message': msg}
|
||||
|
||||
|
||||
@@ -33,9 +33,11 @@
|
||||
${queue_msg|n}
|
||||
</div>
|
||||
|
||||
% if button_enabled:
|
||||
<div class="plot-button">
|
||||
<input type="button" class="save" name="plot-button" id="plot_${id}" value="Plot" />
|
||||
<input type="button" class="save" name="plot-button" id="plot_${id}" value="Run Code" />
|
||||
</div>
|
||||
%endif
|
||||
|
||||
<script>
|
||||
// Note: We need to make the area follow the CodeMirror for this to work.
|
||||
@@ -91,7 +93,7 @@
|
||||
window.location.reload();
|
||||
}
|
||||
else {
|
||||
gentle_alert(problem_elt, msg);
|
||||
gentle_alert(problem_elt, response.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +104,7 @@
|
||||
{'submission': submission}, plot_callback);
|
||||
}
|
||||
else {
|
||||
gentle_alert(problem_elt, msg);
|
||||
gentle_alert(problem_elt, response.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
407
common/lib/capa/capa/tests/test_input_templates.py
Normal file
407
common/lib/capa/capa/tests/test_input_templates.py
Normal file
@@ -0,0 +1,407 @@
|
||||
"""Tests for the logic in input type mako templates."""
|
||||
|
||||
import unittest
|
||||
import capa
|
||||
import os.path
|
||||
from lxml import etree
|
||||
from mako.template import Template as MakoTemplate
|
||||
from mako import exceptions
|
||||
|
||||
|
||||
class TemplateError(Exception):
|
||||
"""Error occurred while rendering a Mako template"""
|
||||
pass
|
||||
|
||||
|
||||
class TemplateTestCase(unittest.TestCase):
|
||||
"""Utilitites for testing templates"""
|
||||
|
||||
# Subclasses override this to specify the file name of the template
|
||||
# to be loaded from capa/templates.
|
||||
# The template name should include the .html extension:
|
||||
# for example: choicegroup.html
|
||||
TEMPLATE_NAME = None
|
||||
|
||||
def setUp(self):
|
||||
"""Load the template"""
|
||||
capa_path = capa.__path__[0]
|
||||
self.template_path = os.path.join(capa_path,
|
||||
'templates',
|
||||
self.TEMPLATE_NAME)
|
||||
template_file = open(self.template_path)
|
||||
self.template = MakoTemplate(template_file.read())
|
||||
template_file.close()
|
||||
|
||||
def render_to_xml(self, context_dict):
|
||||
"""Render the template using the `context_dict` dict.
|
||||
|
||||
Returns an `etree` XML element."""
|
||||
try:
|
||||
xml_str = self.template.render_unicode(**context_dict)
|
||||
except:
|
||||
raise TemplateError(exceptions.text_error_template().render())
|
||||
|
||||
return etree.fromstring(xml_str)
|
||||
|
||||
def assert_has_xpath(self, xml_root, xpath, context_dict, exact_num=1):
|
||||
"""Asserts that the xml tree has an element satisfying `xpath`.
|
||||
|
||||
`xml_root` is an etree XML element
|
||||
`xpath` is an XPath string, such as `'/foo/bar'`
|
||||
`context` is used to print a debugging message
|
||||
`exact_num` is the exact number of matches to expect.
|
||||
"""
|
||||
message = ("XML does not have %d match(es) for xpath '%s'\nXML: %s\nContext: %s"
|
||||
% (exact_num, str(xpath), etree.tostring(xml_root), str(context_dict)))
|
||||
|
||||
self.assertEqual(len(xml_root.xpath(xpath)), exact_num, msg=message)
|
||||
|
||||
def assert_no_xpath(self, xml_root, xpath, context_dict):
|
||||
"""Asserts that the xml tree does NOT have an element
|
||||
satisfying `xpath`.
|
||||
|
||||
`xml_root` is an etree XML element
|
||||
`xpath` is an XPath string, such as `'/foo/bar'`
|
||||
`context` is used to print a debugging message
|
||||
"""
|
||||
self.assert_has_xpath(xml_root, xpath, context_dict, exact_num=0)
|
||||
|
||||
def assert_has_text(self, xml_root, xpath, text, exact=True):
|
||||
"""Find the element at `xpath` in `xml_root` and assert
|
||||
that its text is `text`.
|
||||
|
||||
`xml_root` is an etree XML element
|
||||
`xpath` is an XPath string, such as `'/foo/bar'`
|
||||
`text` is the expected text that the element should contain
|
||||
|
||||
If multiple elements are found, checks the first one.
|
||||
If no elements are found, the assertion fails.
|
||||
"""
|
||||
element_list = xml_root.xpath(xpath)
|
||||
self.assertTrue(len(element_list) > 0,
|
||||
"Could not find element at '%s'" % str(xpath))
|
||||
|
||||
if exact:
|
||||
self.assertEqual(text, element_list[0].text)
|
||||
else:
|
||||
self.assertIn(text, element_list[0].text)
|
||||
|
||||
|
||||
class ChoiceGroupTemplateTest(TemplateTestCase):
|
||||
"""Test mako template for `<choicegroup>` input"""
|
||||
|
||||
TEMPLATE_NAME = 'choicegroup.html'
|
||||
|
||||
def setUp(self):
|
||||
choices = [('1', 'choice 1'), ('2', 'choice 2'), ('3', 'choice 3')]
|
||||
self.context = {'id': '1',
|
||||
'choices': choices,
|
||||
'status': 'correct',
|
||||
'input_type': 'checkbox',
|
||||
'name_array_suffix': '1',
|
||||
'value': '3'}
|
||||
super(ChoiceGroupTemplateTest, self).setUp()
|
||||
|
||||
def test_problem_marked_correct(self):
|
||||
"""Test conditions under which the entire problem
|
||||
(not a particular option) is marked correct"""
|
||||
|
||||
self.context['status'] = 'correct'
|
||||
self.context['input_type'] = 'checkbox'
|
||||
self.context['value'] = ['1', '2']
|
||||
|
||||
# Should mark the entire problem correct
|
||||
xml = self.render_to_xml(self.context)
|
||||
xpath = "//div[@class='indicator_container']/span[@class='correct']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
# Should NOT mark individual options
|
||||
self.assert_no_xpath(xml, "//label[@class='choicegroup_incorrect']",
|
||||
self.context)
|
||||
|
||||
self.assert_no_xpath(xml, "//label[@class='choicegroup_correct']",
|
||||
self.context)
|
||||
|
||||
def test_problem_marked_incorrect(self):
|
||||
"""Test all conditions under which the entire problem
|
||||
(not a particular option) is marked incorrect"""
|
||||
conditions = [
|
||||
{'status': 'incorrect', 'input_type': 'radio', 'value': ''},
|
||||
{'status': 'incorrect', 'input_type': 'checkbox', 'value': []},
|
||||
{'status': 'incorrect', 'input_type': 'checkbox', 'value': ['2']},
|
||||
{'status': 'incorrect', 'input_type': 'checkbox', 'value': ['2', '3']},
|
||||
{'status': 'incomplete', 'input_type': 'radio', 'value': ''},
|
||||
{'status': 'incomplete', 'input_type': 'checkbox', 'value': []},
|
||||
{'status': 'incomplete', 'input_type': 'checkbox', 'value': ['2']},
|
||||
{'status': 'incomplete', 'input_type': 'checkbox', 'value': ['2', '3']}]
|
||||
|
||||
for test_conditions in conditions:
|
||||
self.context.update(test_conditions)
|
||||
xml = self.render_to_xml(self.context)
|
||||
xpath = "//div[@class='indicator_container']/span[@class='incorrect']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
# Should NOT mark individual options
|
||||
self.assert_no_xpath(xml,
|
||||
"//label[@class='choicegroup_incorrect']",
|
||||
self.context)
|
||||
|
||||
self.assert_no_xpath(xml,
|
||||
"//label[@class='choicegroup_correct']",
|
||||
self.context)
|
||||
|
||||
def test_problem_marked_unsubmitted(self):
|
||||
"""Test all conditions under which the entire problem
|
||||
(not a particular option) is marked unanswered"""
|
||||
conditions = [
|
||||
{'status': 'unsubmitted', 'input_type': 'radio', 'value': ''},
|
||||
{'status': 'unsubmitted', 'input_type': 'radio', 'value': []},
|
||||
{'status': 'unsubmitted', 'input_type': 'checkbox', 'value': []},
|
||||
{'input_type': 'radio', 'value': ''},
|
||||
{'input_type': 'radio', 'value': []},
|
||||
{'input_type': 'checkbox', 'value': []},
|
||||
{'input_type': 'checkbox', 'value': ['1']},
|
||||
{'input_type': 'checkbox', 'value': ['1', '2']}]
|
||||
|
||||
self.context['status'] = 'unanswered'
|
||||
|
||||
for test_conditions in conditions:
|
||||
self.context.update(test_conditions)
|
||||
xml = self.render_to_xml(self.context)
|
||||
xpath = "//div[@class='indicator_container']/span[@class='unanswered']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
# Should NOT mark individual options
|
||||
self.assert_no_xpath(xml,
|
||||
"//label[@class='choicegroup_incorrect']",
|
||||
self.context)
|
||||
|
||||
self.assert_no_xpath(xml,
|
||||
"//label[@class='choicegroup_correct']",
|
||||
self.context)
|
||||
|
||||
def test_option_marked_correct(self):
|
||||
"""Test conditions under which a particular option
|
||||
(not the entire problem) is marked correct."""
|
||||
conditions = [
|
||||
{'input_type': 'radio', 'value': '2'},
|
||||
{'input_type': 'radio', 'value': ['2']}]
|
||||
|
||||
self.context['status'] = 'correct'
|
||||
|
||||
for test_conditions in conditions:
|
||||
self.context.update(test_conditions)
|
||||
xml = self.render_to_xml(self.context)
|
||||
xpath = "//label[@class='choicegroup_correct']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
# Should NOT mark the whole problem
|
||||
xpath = "//div[@class='indicator_container']/span"
|
||||
self.assert_no_xpath(xml, xpath, self.context)
|
||||
|
||||
def test_option_marked_incorrect(self):
|
||||
"""Test conditions under which a particular option
|
||||
(not the entire problem) is marked incorrect."""
|
||||
conditions = [
|
||||
{'input_type': 'radio', 'value': '2'},
|
||||
{'input_type': 'radio', 'value': ['2']}]
|
||||
|
||||
self.context['status'] = 'incorrect'
|
||||
|
||||
for test_conditions in conditions:
|
||||
self.context.update(test_conditions)
|
||||
xml = self.render_to_xml(self.context)
|
||||
xpath = "//label[@class='choicegroup_incorrect']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
# Should NOT mark the whole problem
|
||||
xpath = "//div[@class='indicator_container']/span"
|
||||
self.assert_no_xpath(xml, xpath, self.context)
|
||||
|
||||
def test_never_show_correctness(self):
|
||||
"""Test conditions under which we tell the template to
|
||||
NOT show correct/incorrect, but instead show a message.
|
||||
|
||||
This is used, for example, by the Justice course to ask
|
||||
questions without specifying a correct answer. When
|
||||
the student responds, the problem displays "Thank you
|
||||
for your response"
|
||||
"""
|
||||
|
||||
conditions = [
|
||||
{'input_type': 'radio', 'status': 'correct', 'value': ''},
|
||||
{'input_type': 'radio', 'status': 'correct', 'value': '2'},
|
||||
{'input_type': 'radio', 'status': 'correct', 'value': ['2']},
|
||||
{'input_type': 'radio', 'status': 'incorrect', 'value': '2'},
|
||||
{'input_type': 'radio', 'status': 'incorrect', 'value': []},
|
||||
{'input_type': 'radio', 'status': 'incorrect', 'value': ['2']},
|
||||
{'input_type': 'checkbox', 'status': 'correct', 'value': []},
|
||||
{'input_type': 'checkbox', 'status': 'correct', 'value': ['2']},
|
||||
{'input_type': 'checkbox', 'status': 'incorrect', 'value': []},
|
||||
{'input_type': 'checkbox', 'status': 'incorrect', 'value': ['2']}]
|
||||
|
||||
self.context['show_correctness'] = 'never'
|
||||
self.context['submitted_message'] = 'Test message'
|
||||
|
||||
for test_conditions in conditions:
|
||||
self.context.update(test_conditions)
|
||||
xml = self.render_to_xml(self.context)
|
||||
|
||||
# Should NOT mark the entire problem correct/incorrect
|
||||
xpath = "//div[@class='indicator_container']/span[@class='correct']"
|
||||
self.assert_no_xpath(xml, xpath, self.context)
|
||||
|
||||
xpath = "//div[@class='indicator_container']/span[@class='incorrect']"
|
||||
self.assert_no_xpath(xml, xpath, self.context)
|
||||
|
||||
# Should NOT mark individual options
|
||||
self.assert_no_xpath(xml,
|
||||
"//label[@class='choicegroup_incorrect']",
|
||||
self.context)
|
||||
|
||||
self.assert_no_xpath(xml,
|
||||
"//label[@class='choicegroup_correct']",
|
||||
self.context)
|
||||
|
||||
# Expect to see the message
|
||||
self.assert_has_text(xml, "//div[@class='capa_alert']",
|
||||
self.context['submitted_message'])
|
||||
|
||||
def test_no_message_before_submission(self):
|
||||
"""Ensure that we don't show the `submitted_message`
|
||||
before submitting"""
|
||||
|
||||
conditions = [
|
||||
{'input_type': 'radio', 'status': 'unsubmitted', 'value': ''},
|
||||
{'input_type': 'radio', 'status': 'unsubmitted', 'value': []},
|
||||
{'input_type': 'checkbox', 'status': 'unsubmitted', 'value': []},
|
||||
|
||||
# These tests expose bug #365
|
||||
# When the bug is fixed, uncomment these cases.
|
||||
#{'input_type': 'radio', 'status': 'unsubmitted', 'value': '2'},
|
||||
#{'input_type': 'radio', 'status': 'unsubmitted', 'value': ['2']},
|
||||
#{'input_type': 'radio', 'status': 'unsubmitted', 'value': '2'},
|
||||
#{'input_type': 'radio', 'status': 'unsubmitted', 'value': ['2']},
|
||||
#{'input_type': 'checkbox', 'status': 'unsubmitted', 'value': ['2']},
|
||||
#{'input_type': 'checkbox', 'status': 'unsubmitted', 'value': ['2']}]
|
||||
]
|
||||
|
||||
self.context['show_correctness'] = 'never'
|
||||
self.context['submitted_message'] = 'Test message'
|
||||
|
||||
for test_conditions in conditions:
|
||||
self.context.update(test_conditions)
|
||||
xml = self.render_to_xml(self.context)
|
||||
|
||||
# Expect that we do NOT see the message yet
|
||||
self.assert_no_xpath(xml, "//div[@class='capa_alert']", self.context)
|
||||
|
||||
|
||||
class TextlineTemplateTest(TemplateTestCase):
|
||||
"""Test mako template for `<textline>` input"""
|
||||
|
||||
TEMPLATE_NAME = 'textline.html'
|
||||
|
||||
def setUp(self):
|
||||
self.context = {'id': '1',
|
||||
'status': 'correct',
|
||||
'value': '3',
|
||||
'preprocessor': None,
|
||||
'trailing_text': None}
|
||||
super(TextlineTemplateTest, self).setUp()
|
||||
|
||||
def test_section_class(self):
|
||||
cases = [({}, ' capa_inputtype '),
|
||||
({'do_math': True}, 'text-input-dynamath capa_inputtype '),
|
||||
({'inline': True}, ' capa_inputtype inline'),
|
||||
({'do_math': True, 'inline': True}, 'text-input-dynamath capa_inputtype inline'), ]
|
||||
|
||||
for (context, css_class) in cases:
|
||||
base_context = self.context.copy()
|
||||
base_context.update(context)
|
||||
xml = self.render_to_xml(base_context)
|
||||
xpath = "//section[@class='%s']" % css_class
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
def test_status(self):
|
||||
cases = [('correct', 'correct', 'correct'),
|
||||
('unsubmitted', 'unanswered', 'unanswered'),
|
||||
('incorrect', 'incorrect', 'incorrect'),
|
||||
('incomplete', 'incorrect', 'incomplete')]
|
||||
|
||||
for (context_status, div_class, status_mark) in cases:
|
||||
self.context['status'] = context_status
|
||||
xml = self.render_to_xml(self.context)
|
||||
|
||||
# Expect that we get a <div> with correct class
|
||||
xpath = "//div[@class='%s ']" % div_class
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
# Expect that we get a <p> with class="status"
|
||||
# (used to by CSS to draw the green check / red x)
|
||||
self.assert_has_text(xml, "//p[@class='status']",
|
||||
status_mark, exact=False)
|
||||
|
||||
def test_hidden(self):
|
||||
self.context['hidden'] = True
|
||||
xml = self.render_to_xml(self.context)
|
||||
|
||||
xpath = "//div[@style='display:none;']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
xpath = "//input[@style='display:none;']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
def test_do_math(self):
|
||||
self.context['do_math'] = True
|
||||
xml = self.render_to_xml(self.context)
|
||||
|
||||
xpath = "//input[@class='math']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
xpath = "//div[@class='equation']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
xpath = "//textarea[@id='input_1_dynamath']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
def test_size(self):
|
||||
self.context['size'] = '20'
|
||||
xml = self.render_to_xml(self.context)
|
||||
|
||||
xpath = "//input[@size='20']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
def test_preprocessor(self):
|
||||
self.context['preprocessor'] = {'class_name': 'test_class',
|
||||
'script_src': 'test_script'}
|
||||
xml = self.render_to_xml(self.context)
|
||||
|
||||
xpath = "//div[@class='text-input-dynamath_data' and @data-preprocessor='test_class']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
xpath = "//div[@class='script_placeholder' and @data-src='test_script']"
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
def test_do_inline(self):
|
||||
cases = [('correct', 'correct'),
|
||||
('unsubmitted', 'unanswered'),
|
||||
('incorrect', 'incorrect'),
|
||||
('incomplete', 'incorrect')]
|
||||
|
||||
self.context['inline'] = True
|
||||
|
||||
for (context_status, div_class) in cases:
|
||||
self.context['status'] = context_status
|
||||
xml = self.render_to_xml(self.context)
|
||||
|
||||
# Expect that we get a <div> with correct class
|
||||
xpath = "//div[@class='%s inline']" % div_class
|
||||
self.assert_has_xpath(xml, xpath, self.context)
|
||||
|
||||
def test_message(self):
|
||||
self.context['msg'] = "Test message"
|
||||
xml = self.render_to_xml(self.context)
|
||||
|
||||
xpath = "//span[@class='message']"
|
||||
self.assert_has_text(xml, xpath, self.context['msg'])
|
||||
@@ -384,6 +384,7 @@ class MatlabTest(unittest.TestCase):
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'button_enabled': True,
|
||||
'queue_len': '3'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
@@ -409,10 +410,37 @@ class MatlabTest(unittest.TestCase):
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'button_enabled': True,
|
||||
'queue_len': '3'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
def test_rendering_when_completed(self):
|
||||
for status in ['correct', 'incorrect']:
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': status,
|
||||
'input_state': {},
|
||||
}
|
||||
elt = etree.fromstring(self.xml)
|
||||
|
||||
the_input = self.input_class(test_system, elt, state)
|
||||
context = the_input._get_render_context()
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': status,
|
||||
'msg': '',
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': '',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'button_enabled': False,
|
||||
'queue_len': '0'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
def test_rendering_while_queued(self):
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
@@ -433,6 +461,7 @@ class MatlabTest(unittest.TestCase):
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'button_enabled': True,
|
||||
'queue_len': '1'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
@@ -447,6 +476,17 @@ class MatlabTest(unittest.TestCase):
|
||||
self.assertTrue(self.the_input.input_state['queuekey'] is not None)
|
||||
self.assertEqual(self.the_input.input_state['queuestate'], 'queued')
|
||||
|
||||
def test_plot_data_failure(self):
|
||||
get = {'submission': 'x = 1234;'}
|
||||
error_message = 'Error message!'
|
||||
test_system.xqueue['interface'].send_to_queue.return_value = (1, error_message)
|
||||
response = self.the_input.handle_ajax("plot", get)
|
||||
self.assertFalse(response['success'])
|
||||
self.assertEqual(response['message'], error_message)
|
||||
self.assertTrue('queuekey' not in self.the_input.input_state)
|
||||
self.assertTrue('queuestate' not in self.the_input.input_state)
|
||||
test_system.xqueue['interface'].send_to_queue.return_value = (0, 'Success!')
|
||||
|
||||
def test_ungraded_response_success(self):
|
||||
queuekey = 'abcd'
|
||||
input_state = {'queuekey': queuekey, 'queuestate': 'queued'}
|
||||
@@ -583,7 +623,6 @@ class ImageInputTest(unittest.TestCase):
|
||||
self.check('[12 13 14]', 0, 0)
|
||||
|
||||
|
||||
|
||||
class CrystallographyTest(unittest.TestCase):
|
||||
'''
|
||||
Check that crystallography inputs work
|
||||
@@ -613,8 +652,7 @@ class CrystallographyTest(unittest.TestCase):
|
||||
'status': 'unsubmitted',
|
||||
'msg': '',
|
||||
'width': width,
|
||||
'height': height,
|
||||
}
|
||||
'height': height}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
@@ -654,13 +692,11 @@ class VseprTest(unittest.TestCase):
|
||||
'width': width,
|
||||
'height': height,
|
||||
'molecules': molecules,
|
||||
'geometries': geometries,
|
||||
}
|
||||
'geometries': geometries}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
|
||||
class ChemicalEquationTest(unittest.TestCase):
|
||||
'''
|
||||
Check that chemical equation inputs work.
|
||||
@@ -674,7 +710,6 @@ class ChemicalEquationTest(unittest.TestCase):
|
||||
state = {'value': 'H2OYeah', }
|
||||
self.the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
|
||||
|
||||
|
||||
def test_rendering(self):
|
||||
''' Verify that the render context matches the expected render context'''
|
||||
context = self.the_input._get_render_context()
|
||||
@@ -688,10 +723,8 @@ class ChemicalEquationTest(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
def test_chemcalc_ajax_sucess(self):
|
||||
''' Verify that using the correct dispatch and valid data produces a valid response'''
|
||||
|
||||
data = {'formula': "H"}
|
||||
response = self.the_input.handle_ajax("preview_chemcalc", data)
|
||||
|
||||
@@ -700,9 +733,6 @@ class ChemicalEquationTest(unittest.TestCase):
|
||||
self.assertEqual(response['error'], "")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class DragAndDropTest(unittest.TestCase):
|
||||
'''
|
||||
Check that drag and drop inputs work
|
||||
|
||||
@@ -274,7 +274,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
# now import any 'draft' items
|
||||
if draft_store is not None:
|
||||
import_course_draft(xml_module_store, draft_store, course_data_path,
|
||||
import_course_draft(xml_module_store, store, draft_store, course_data_path,
|
||||
static_content_store, target_location_namespace if target_location_namespace is not None
|
||||
else course_location)
|
||||
|
||||
@@ -339,7 +339,7 @@ def import_module(module, store, course_data_path, static_content_store, allow_n
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
|
||||
|
||||
def import_course_draft(xml_module_store, store, course_data_path, static_content_store, target_location_namespace):
|
||||
def import_course_draft(xml_module_store, store, draft_store, course_data_path, static_content_store, target_location_namespace):
|
||||
'''
|
||||
This will import all the content inside of the 'drafts' folder, if it exists
|
||||
NOTE: This is not a full course import, basically in our current application only verticals (and downwards)
|
||||
@@ -396,7 +396,7 @@ def import_course_draft(xml_module_store, store, course_data_path, static_conten
|
||||
del module.xml_attributes['parent_sequential_url']
|
||||
del module.xml_attributes['index_in_children_list']
|
||||
|
||||
import_module(module, store, course_data_path, static_content_store, allow_not_found=True)
|
||||
import_module(module, draft_store, course_data_path, static_content_store, allow_not_found=True)
|
||||
for child in module.get_children():
|
||||
_import_module(child)
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
"""Tests of the Capa XModule"""
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=R0904
|
||||
#pylint: disable=C0103
|
||||
#pylint: disable=C0302
|
||||
|
||||
import datetime
|
||||
import json
|
||||
from mock import Mock, MagicMock, patch
|
||||
from pprint import pprint
|
||||
from mock import Mock, patch
|
||||
import unittest
|
||||
import random
|
||||
|
||||
import xmodule
|
||||
import capa
|
||||
from capa.responsetypes import StudentInputError, \
|
||||
LoncapaProblemError, ResponseError
|
||||
LoncapaProblemError, ResponseError
|
||||
from xmodule.capa_module import CapaModule
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
|
||||
from django.http import QueryDict
|
||||
|
||||
@@ -384,7 +386,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# what the input is, by patching CorrectMap.is_correct()
|
||||
# Also simulate rendering the HTML
|
||||
# TODO: pep8 thinks the following line has invalid syntax
|
||||
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct,\
|
||||
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct, \
|
||||
patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
|
||||
mock_is_correct.return_value = True
|
||||
mock_html.return_value = "Test HTML"
|
||||
@@ -435,32 +437,38 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertEqual(module.attempts, 3)
|
||||
|
||||
def test_check_problem_resubmitted_with_randomize(self):
|
||||
# Randomize turned on
|
||||
module = CapaFactory.create(rerandomize='always', attempts=0)
|
||||
rerandomize_values = ['always', 'true']
|
||||
|
||||
# Simulate that the problem is completed
|
||||
module.done = True
|
||||
for rerandomize in rerandomize_values:
|
||||
# Randomize turned on
|
||||
module = CapaFactory.create(rerandomize=rerandomize, attempts=0)
|
||||
|
||||
# Expect that we cannot submit
|
||||
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
module.check_problem(get_request_dict)
|
||||
# Simulate that the problem is completed
|
||||
module.done = True
|
||||
|
||||
# Expect that number of attempts NOT incremented
|
||||
self.assertEqual(module.attempts, 0)
|
||||
# Expect that we cannot submit
|
||||
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that number of attempts NOT incremented
|
||||
self.assertEqual(module.attempts, 0)
|
||||
|
||||
def test_check_problem_resubmitted_no_randomize(self):
|
||||
# Randomize turned off
|
||||
module = CapaFactory.create(rerandomize='never', attempts=0, done=True)
|
||||
rerandomize_values = ['never', 'false', 'per_student']
|
||||
|
||||
# Expect that we can submit successfully
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
for rerandomize in rerandomize_values:
|
||||
# Randomize turned off
|
||||
module = CapaFactory.create(rerandomize=rerandomize, attempts=0, done=True)
|
||||
|
||||
self.assertEqual(result['success'], 'correct')
|
||||
# Expect that we can submit successfully
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that number of attempts IS incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
self.assertEqual(result['success'], 'correct')
|
||||
|
||||
# Expect that number of attempts IS incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
def test_check_problem_queued(self):
|
||||
module = CapaFactory.create(attempts=1)
|
||||
@@ -615,24 +623,34 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
def test_save_problem_submitted_with_randomize(self):
|
||||
module = CapaFactory.create(rerandomize='always', done=True)
|
||||
|
||||
# Try to save
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
# Capa XModule treats 'always' and 'true' equivalently
|
||||
rerandomize_values = ['always', 'true']
|
||||
|
||||
# Expect that we cannot save
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
for rerandomize in rerandomize_values:
|
||||
module = CapaFactory.create(rerandomize=rerandomize, done=True)
|
||||
|
||||
# Try to save
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that we cannot save
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
def test_save_problem_submitted_no_randomize(self):
|
||||
module = CapaFactory.create(rerandomize='never', done=True)
|
||||
|
||||
# Try to save
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
# Capa XModule treats 'false' and 'per_student' equivalently
|
||||
rerandomize_values = ['never', 'false', 'per_student']
|
||||
|
||||
# Expect that we succeed
|
||||
self.assertTrue('success' in result and result['success'])
|
||||
for rerandomize in rerandomize_values:
|
||||
module = CapaFactory.create(rerandomize=rerandomize, done=True)
|
||||
|
||||
# Try to save
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that we succeed
|
||||
self.assertTrue('success' in result and result['success'])
|
||||
|
||||
def test_check_button_name(self):
|
||||
|
||||
@@ -681,21 +699,30 @@ class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
# If user submitted a problem but hasn't reset,
|
||||
# do NOT show the check button
|
||||
# Note: we can only reset when rerandomize="always"
|
||||
# Note: we can only reset when rerandomize="always" or "true"
|
||||
module = CapaFactory.create(rerandomize="always", done=True)
|
||||
self.assertFalse(module.should_show_check_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="true", done=True)
|
||||
self.assertFalse(module.should_show_check_button())
|
||||
|
||||
# Otherwise, DO show the check button
|
||||
module = CapaFactory.create()
|
||||
self.assertTrue(module.should_show_check_button())
|
||||
|
||||
# If the user has submitted the problem
|
||||
# and we do NOT have a reset button, then we can show the check button
|
||||
# Setting rerandomize to "never" ensures that the reset button
|
||||
# Setting rerandomize to "never" or "false" ensures that the reset button
|
||||
# is not shown
|
||||
module = CapaFactory.create(rerandomize="never", done=True)
|
||||
self.assertTrue(module.should_show_check_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="false", done=True)
|
||||
self.assertTrue(module.should_show_check_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="per_student", done=True)
|
||||
self.assertTrue(module.should_show_check_button())
|
||||
|
||||
def test_should_show_reset_button(self):
|
||||
|
||||
attempts = random.randint(1, 10)
|
||||
@@ -712,6 +739,14 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module = CapaFactory.create(rerandomize="never", done=True)
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# If we're NOT randomizing, then do NOT show the reset button
|
||||
module = CapaFactory.create(rerandomize="per_student", done=True)
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# If we're NOT randomizing, then do NOT show the reset button
|
||||
module = CapaFactory.create(rerandomize="false", done=True)
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# If the user hasn't submitted an answer yet,
|
||||
# then do NOT show the reset button
|
||||
module = CapaFactory.create(done=False)
|
||||
@@ -742,13 +777,19 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module = CapaFactory.create(rerandomize="always", done=True)
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="true", done=True)
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
# If the user has unlimited attempts and we are not randomizing,
|
||||
# then do NOT show a save button
|
||||
# because they can keep using "Check"
|
||||
module = CapaFactory.create(max_attempts=None, rerandomize="never", done=False)
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
module = CapaFactory.create(max_attempts=None, rerandomize="never", done=True)
|
||||
module = CapaFactory.create(max_attempts=None, rerandomize="false", done=True)
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
module = CapaFactory.create(max_attempts=None, rerandomize="per_student", done=True)
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
# Otherwise, DO show the save button
|
||||
@@ -759,6 +800,12 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module = CapaFactory.create(rerandomize="never", max_attempts=2, done=True)
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="false", max_attempts=2, done=True)
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="per_student", max_attempts=2, done=True)
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
# If survey question for capa (max_attempts = 0),
|
||||
# DO show the save button
|
||||
module = CapaFactory.create(max_attempts=0, done=False)
|
||||
@@ -788,9 +835,15 @@ class CapaModuleTest(unittest.TestCase):
|
||||
done=True)
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
module = CapaFactory.create(force_save_button="true",
|
||||
rerandomize="true",
|
||||
done=True)
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
def test_no_max_attempts(self):
|
||||
module = CapaFactory.create(max_attempts='')
|
||||
html = module.get_problem_html()
|
||||
self.assertTrue(html is not None)
|
||||
# assert that we got here without exploding
|
||||
|
||||
def test_get_problem_html(self):
|
||||
@@ -875,6 +928,8 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Try to render the module with DEBUG turned off
|
||||
html = module.get_problem_html()
|
||||
|
||||
self.assertTrue(html is not None)
|
||||
|
||||
# Check the rendering context
|
||||
render_args, _ = module.system.render_template.call_args
|
||||
context = render_args[1]
|
||||
@@ -886,7 +941,9 @@ class CapaModuleTest(unittest.TestCase):
|
||||
def test_random_seed_no_change(self):
|
||||
|
||||
# Run the test for each possible rerandomize value
|
||||
for rerandomize in ['never', 'per_student', 'always', 'onreset']:
|
||||
for rerandomize in ['false', 'never',
|
||||
'per_student', 'always',
|
||||
'true', 'onreset']:
|
||||
module = CapaFactory.create(rerandomize=rerandomize)
|
||||
|
||||
# Get the seed
|
||||
@@ -896,8 +953,9 @@ class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
# If we're not rerandomizing, the seed is always set
|
||||
# to the same value (1)
|
||||
if rerandomize == 'never':
|
||||
self.assertEqual(seed, 1)
|
||||
if rerandomize in ['never']:
|
||||
self.assertEqual(seed, 1,
|
||||
msg="Seed should always be 1 when rerandomize='%s'" % rerandomize)
|
||||
|
||||
# Check the problem
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
@@ -947,7 +1005,8 @@ class CapaModuleTest(unittest.TestCase):
|
||||
return success
|
||||
|
||||
# Run the test for each possible rerandomize value
|
||||
for rerandomize in ['never', 'per_student', 'always', 'onreset']:
|
||||
for rerandomize in ['never', 'false', 'per_student',
|
||||
'always', 'true', 'onreset']:
|
||||
module = CapaFactory.create(rerandomize=rerandomize)
|
||||
|
||||
# Get the seed
|
||||
@@ -959,7 +1018,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# is set to 'never' -- it should still be 1
|
||||
# The seed also stays the same if we're randomizing
|
||||
# 'per_student': the same student should see the same problem
|
||||
if rerandomize in ['never', 'per_student']:
|
||||
if rerandomize in ['never', 'false', 'per_student']:
|
||||
self.assertEqual(seed, _reset_and_get_seed(module))
|
||||
|
||||
# Otherwise, we expect the seed to change
|
||||
@@ -969,10 +1028,8 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Since there's a small chance we might get the
|
||||
# same seed again, give it 5 chances
|
||||
# to generate a different seed
|
||||
success = _retry_and_check(5,
|
||||
lambda: _reset_and_get_seed(module) != seed)
|
||||
success = _retry_and_check(5, lambda: _reset_and_get_seed(module) != seed)
|
||||
|
||||
# TODO: change this comparison to module.seed is not None?
|
||||
self.assertTrue(module.seed != None)
|
||||
self.assertTrue(module.seed is not None)
|
||||
msg = 'Could not get a new seed from reset after 5 tries'
|
||||
self.assertTrue(success, msg)
|
||||
|
||||
@@ -88,20 +88,32 @@ if Backbone?
|
||||
pinned = @get("pinned")
|
||||
@set("pinned",pinned)
|
||||
@trigger "change", @
|
||||
|
||||
flagAbuse: ->
|
||||
temp_array = @get("abuse_flaggers")
|
||||
temp_array.push(window.user.get('id'))
|
||||
@set("abuse_flaggers",temp_array)
|
||||
@trigger "change", @
|
||||
|
||||
unflagAbuse: ->
|
||||
@get("abuse_flaggers").pop(window.user.get('id'))
|
||||
@trigger "change", @
|
||||
|
||||
|
||||
class @Thread extends @Content
|
||||
urlMappers:
|
||||
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
|
||||
'reply' : -> DiscussionUtil.urlFor('create_comment', @id)
|
||||
'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
|
||||
'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
|
||||
'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
|
||||
'close' : -> DiscussionUtil.urlFor('openclose_thread', @id)
|
||||
'update' : -> DiscussionUtil.urlFor('update_thread', @id)
|
||||
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
|
||||
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
|
||||
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
|
||||
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
|
||||
'reply' : -> DiscussionUtil.urlFor('create_comment', @id)
|
||||
'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
|
||||
'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
|
||||
'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
|
||||
'close' : -> DiscussionUtil.urlFor('openclose_thread', @id)
|
||||
'update' : -> DiscussionUtil.urlFor('update_thread', @id)
|
||||
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
|
||||
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
|
||||
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
|
||||
'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id)
|
||||
'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @id)
|
||||
'pinThread' : -> DiscussionUtil.urlFor("pin_thread", @id)
|
||||
'unPinThread' : -> DiscussionUtil.urlFor("un_pin_thread", @id)
|
||||
|
||||
@@ -157,6 +169,8 @@ if Backbone?
|
||||
'endorse': -> DiscussionUtil.urlFor('endorse_comment', @id)
|
||||
'update': -> DiscussionUtil.urlFor('update_comment', @id)
|
||||
'delete': -> DiscussionUtil.urlFor('delete_comment', @id)
|
||||
'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id)
|
||||
'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @id)
|
||||
|
||||
getCommentsCount: ->
|
||||
count = 0
|
||||
|
||||
@@ -37,6 +37,9 @@ if Backbone?
|
||||
data['commentable_ids'] = options.commentable_ids
|
||||
when 'all'
|
||||
url = DiscussionUtil.urlFor 'threads'
|
||||
when 'flagged'
|
||||
data['flagged'] = true
|
||||
url = DiscussionUtil.urlFor 'search'
|
||||
when 'followed'
|
||||
url = DiscussionUtil.urlFor 'followed_threads', options.user_id
|
||||
if options['group_id']
|
||||
|
||||
@@ -18,8 +18,12 @@ class @DiscussionUtil
|
||||
@loadRoles: (roles)->
|
||||
@roleIds = roles
|
||||
|
||||
@loadFlagModerator: (what)->
|
||||
@isFlagModerator = what
|
||||
|
||||
@loadRolesFromContainer: ->
|
||||
@loadRoles($("#discussion-container").data("roles"))
|
||||
@loadFlagModerator($("#discussion-container").data("flag-moderator"))
|
||||
|
||||
@isStaff: (user_id) ->
|
||||
staff = _.union(@roleIds['Staff'], @roleIds['Moderator'], @roleIds['Administrator'])
|
||||
@@ -48,6 +52,10 @@ class @DiscussionUtil
|
||||
update_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/update"
|
||||
create_comment : "/courses/#{$$course_id}/discussion/threads/#{param}/reply"
|
||||
delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete"
|
||||
flagAbuse_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/flagAbuse"
|
||||
unFlagAbuse_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unFlagAbuse"
|
||||
flagAbuse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/flagAbuse"
|
||||
unFlagAbuse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/unFlagAbuse"
|
||||
upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote"
|
||||
downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote"
|
||||
pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/pin"
|
||||
@@ -72,7 +80,7 @@ class @DiscussionUtil
|
||||
permanent_link_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
|
||||
permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}"
|
||||
user_profile : "/courses/#{$$course_id}/discussion/forum/users/#{param}"
|
||||
followed_threads : "/courses/#{$$course_id}/discussion/forum/users/#{param}/followed"
|
||||
followed_threads : "/courses/#{$$course_id}/discussion/forum/users/#{param}/followed"
|
||||
threads : "/courses/#{$$course_id}/discussion/forum"
|
||||
}[name]
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
if Backbone?
|
||||
class @DiscussionContentView extends Backbone.View
|
||||
|
||||
|
||||
events:
|
||||
"click .discussion-flag-abuse": "toggleFlagAbuse"
|
||||
|
||||
|
||||
attrRenderer:
|
||||
endorsed: (endorsed) ->
|
||||
if endorsed
|
||||
@@ -94,7 +99,48 @@ if Backbone?
|
||||
|
||||
setWmdContent: (cls_identifier, text) =>
|
||||
DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), cls_identifier, text
|
||||
|
||||
|
||||
initialize: ->
|
||||
@initLocal()
|
||||
@model.bind('change', @renderPartialAttrs, @)
|
||||
|
||||
|
||||
|
||||
toggleFlagAbuse: (event) ->
|
||||
event.preventDefault()
|
||||
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
|
||||
@unFlagAbuse()
|
||||
else
|
||||
@flagAbuse()
|
||||
|
||||
flagAbuse: ->
|
||||
url = @model.urlFor("flagAbuse")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-flag-abuse")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
###
|
||||
note, we have to clone the array in order to trigger a change event
|
||||
###
|
||||
temp_array = _.clone(@model.get('abuse_flaggers'));
|
||||
temp_array.push(window.user.id)
|
||||
@model.set('abuse_flaggers', temp_array)
|
||||
|
||||
unFlagAbuse: ->
|
||||
url = @model.urlFor("unFlagAbuse")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-flag-abuse")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
temp_array = _.clone(@model.get('abuse_flaggers'));
|
||||
temp_array.pop(window.user.id)
|
||||
# if you're an admin, clear this
|
||||
if DiscussionUtil.isFlagModerator
|
||||
temp_array = []
|
||||
|
||||
@model.set('abuse_flaggers', temp_array)
|
||||
|
||||
@@ -276,6 +276,11 @@ if Backbone?
|
||||
@$(".post-search-field").val("")
|
||||
@$('.cohort').show()
|
||||
@retrieveAllThreads()
|
||||
else if discussionId == "#flagged"
|
||||
@discussionIds = ""
|
||||
@$(".post-search-field").val("")
|
||||
@$('.cohort').hide()
|
||||
@retrieveFlaggedThreads()
|
||||
else if discussionId == "#following"
|
||||
@retrieveFollowed(event)
|
||||
@$('.cohort').hide()
|
||||
@@ -321,6 +326,12 @@ if Backbone?
|
||||
@collection.reset()
|
||||
@loadMorePages(event)
|
||||
|
||||
retrieveFlaggedThreads: (event)->
|
||||
@collection.current_page = 0
|
||||
@collection.reset()
|
||||
@mode = 'flagged'
|
||||
@loadMorePages(event)
|
||||
|
||||
sortThreads: (event) ->
|
||||
@$(".sort-bar a").removeClass("active")
|
||||
$(event.target).addClass("active")
|
||||
|
||||
@@ -3,6 +3,7 @@ if Backbone?
|
||||
|
||||
events:
|
||||
"click .discussion-vote": "toggleVote"
|
||||
"click .discussion-flag-abuse": "toggleFlagAbuse"
|
||||
"click .admin-pin": "togglePin"
|
||||
"click .action-follow": "toggleFollowing"
|
||||
"click .action-edit": "edit"
|
||||
@@ -25,6 +26,7 @@ if Backbone?
|
||||
@delegateEvents()
|
||||
@renderDogear()
|
||||
@renderVoted()
|
||||
@renderFlagged()
|
||||
@renderPinned()
|
||||
@renderAttrs()
|
||||
@$("span.timeago").timeago()
|
||||
@@ -42,6 +44,16 @@ if Backbone?
|
||||
@$("[data-role=discussion-vote]").addClass("is-cast")
|
||||
else
|
||||
@$("[data-role=discussion-vote]").removeClass("is-cast")
|
||||
|
||||
renderFlagged: =>
|
||||
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
|
||||
@$("[data-role=thread-flag]").addClass("flagged")
|
||||
@$("[data-role=thread-flag]").removeClass("notflagged")
|
||||
@$(".discussion-flag-abuse .flag-label").html("Misuse Reported")
|
||||
else
|
||||
@$("[data-role=thread-flag]").removeClass("flagged")
|
||||
@$("[data-role=thread-flag]").addClass("notflagged")
|
||||
@$(".discussion-flag-abuse .flag-label").html("Report Misuse")
|
||||
|
||||
renderPinned: =>
|
||||
if @model.get("pinned")
|
||||
@@ -56,6 +68,7 @@ if Backbone?
|
||||
|
||||
updateModelDetails: =>
|
||||
@renderVoted()
|
||||
@renderFlagged()
|
||||
@renderPinned()
|
||||
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
|
||||
|
||||
@@ -96,6 +109,7 @@ if Backbone?
|
||||
if textStatus == 'success'
|
||||
@model.set(response, {silent: true})
|
||||
|
||||
|
||||
unvote: ->
|
||||
window.user.unvote(@model)
|
||||
url = @model.urlFor("unvote")
|
||||
@@ -107,6 +121,7 @@ if Backbone?
|
||||
if textStatus == 'success'
|
||||
@model.set(response, {silent: true})
|
||||
|
||||
|
||||
edit: (event) ->
|
||||
@trigger "thread:edit", event
|
||||
|
||||
@@ -182,4 +197,4 @@ if Backbone?
|
||||
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
|
||||
Mustache.render(@template, params)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ if Backbone?
|
||||
body = @getWmdContent("reply-body")
|
||||
return if not body.trim().length
|
||||
@setWmdContent("reply-body", "")
|
||||
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), votes: { up_count: 0 }, endorsed: false, user_id: window.user.get("id"))
|
||||
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), votes: { up_count: 0 }, abuse_flaggers:[], endorsed: false, user_id: window.user.get("id"))
|
||||
comment.set('thread', @model.get('thread'))
|
||||
@renderResponse(comment)
|
||||
@model.addComment()
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
if Backbone?
|
||||
class @ResponseCommentShowView extends DiscussionContentView
|
||||
|
||||
events:
|
||||
"click .discussion-flag-abuse": "toggleFlagAbuse"
|
||||
|
||||
tagName: "li"
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
@model.on "change", @updateModelDetails
|
||||
|
||||
render: ->
|
||||
@template = _.template($("#response-comment-show-template").html())
|
||||
@@ -11,6 +18,7 @@ if Backbone?
|
||||
@initLocal()
|
||||
@delegateEvents()
|
||||
@renderAttrs()
|
||||
@renderFlagged()
|
||||
@markAsStaff()
|
||||
@$el.find(".timeago").timeago()
|
||||
@convertMath()
|
||||
@@ -34,3 +42,17 @@ if Backbone?
|
||||
@$el.find("a.profile-link").after('<span class="staff-label">staff</span>')
|
||||
else if DiscussionUtil.isTA(@model.get("user_id"))
|
||||
@$el.find("a.profile-link").after('<span class="community-ta-label">Community TA</span>')
|
||||
|
||||
|
||||
renderFlagged: =>
|
||||
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
|
||||
@$("[data-role=thread-flag]").addClass("flagged")
|
||||
@$("[data-role=thread-flag]").removeClass("notflagged")
|
||||
else
|
||||
@$("[data-role=thread-flag]").removeClass("flagged")
|
||||
@$("[data-role=thread-flag]").addClass("notflagged")
|
||||
|
||||
updateModelDetails: =>
|
||||
@renderFlagged()
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ if Backbone?
|
||||
"click .action-endorse": "toggleEndorse"
|
||||
"click .action-delete": "delete"
|
||||
"click .action-edit": "edit"
|
||||
"click .discussion-flag-abuse": "toggleFlagAbuse"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
@@ -23,6 +24,7 @@ if Backbone?
|
||||
if window.user.voted(@model)
|
||||
@$(".vote-btn").addClass("is-cast")
|
||||
@renderAttrs()
|
||||
@renderFlagged()
|
||||
@$el.find(".posted-details").timeago()
|
||||
@convertMath()
|
||||
@markAsStaff()
|
||||
@@ -70,6 +72,7 @@ if Backbone?
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set(response)
|
||||
|
||||
|
||||
edit: (event) ->
|
||||
@trigger "response:edit", event
|
||||
@@ -92,3 +95,17 @@ if Backbone?
|
||||
url: url
|
||||
data: data
|
||||
type: "POST"
|
||||
|
||||
|
||||
renderFlagged: =>
|
||||
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
|
||||
@$("[data-role=thread-flag]").addClass("flagged")
|
||||
@$("[data-role=thread-flag]").removeClass("notflagged")
|
||||
@$(".discussion-flag-abuse .flag-label").html("Misuse Reported")
|
||||
else
|
||||
@$("[data-role=thread-flag]").removeClass("flagged")
|
||||
@$("[data-role=thread-flag]").addClass("notflagged")
|
||||
@$(".discussion-flag-abuse .flag-label").html("Report Misuse")
|
||||
|
||||
updateModelDetails: =>
|
||||
@renderFlagged()
|
||||
|
||||
@@ -77,7 +77,7 @@ if Backbone?
|
||||
body = @getWmdContent("comment-body")
|
||||
return if not body.trim().length
|
||||
@setWmdContent("comment-body", "")
|
||||
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), user_id: window.user.get("id"), id:"unsaved")
|
||||
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), abuse_flaggers:[], user_id: window.user.get("id"), id:"unsaved")
|
||||
view = @renderComment(comment)
|
||||
@hideEditorChrome()
|
||||
@trigger "comment:add", comment
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -12,7 +12,6 @@ This will read the `Gemfile` and install all of the gems specified there.
|
||||
Run the following::
|
||||
|
||||
pip install -r requirements.txt
|
||||
pip install -r test-requirements.txt
|
||||
|
||||
### Binaries
|
||||
|
||||
@@ -52,77 +51,13 @@ or with additional options:
|
||||
|
||||
*N.B.* You may have to escape the `[` characters, depending on your shell: `rake "lms[test,5000]"`
|
||||
|
||||
## Running tests
|
||||
|
||||
### Python Tests
|
||||
|
||||
This runs all the tests (long, uses collectstatic):
|
||||
|
||||
rake test
|
||||
|
||||
If if you aren't changing static files, can run `rake test` once, then run
|
||||
|
||||
rake fasttest_lms
|
||||
|
||||
or
|
||||
|
||||
rake fasttest_cms
|
||||
|
||||
xmodule can be tested independently, with this:
|
||||
|
||||
rake test_common/lib/xmodule
|
||||
|
||||
To run a single django test class:
|
||||
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth
|
||||
|
||||
To run a single django test:
|
||||
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth.test_dark_launch
|
||||
|
||||
|
||||
To run a single nose test file:
|
||||
|
||||
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py
|
||||
|
||||
To run a single nose test:
|
||||
|
||||
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify
|
||||
|
||||
|
||||
Very handy: if you uncomment the `pdb=1` line in `setup.cfg`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html
|
||||
|
||||
|
||||
### Javascript Tests
|
||||
|
||||
These commands start a development server with jasmine testing enabled, and launch your default browser
|
||||
pointing to those tests
|
||||
|
||||
rake browse_jasmine_{lms,cms}
|
||||
|
||||
To run the tests headless, you must install phantomjs (http://phantomjs.org/download.html).
|
||||
|
||||
rake phantomjs_jasmine_{lms,cms}
|
||||
|
||||
If the `phantomjs` binary is not on the path, set the `PHANTOMJS_PATH` environment variable to point to it
|
||||
|
||||
PHANTOMJS_PATH=/path/to/phantomjs rake phantomjs_jasmine_{lms,cms}
|
||||
|
||||
|
||||
## Getting More Information
|
||||
|
||||
Run the following to see a list of all rake tasks available and their arguments
|
||||
To get a full list of available rake tasks, use:
|
||||
|
||||
rake -T
|
||||
|
||||
## Testing using queue servers
|
||||
|
||||
When testing problems that use a queue server on AWS (e.g. sandbox-xqueue.edx.org), you'll need to run your server on your public IP, like so.
|
||||
|
||||
`django-admin.py runserver --settings=lms.envs.dev --pythonpath=. 0.0.0.0:8000`
|
||||
|
||||
When you connect to the LMS, you need to use the public ip. Use `ifconfig` to figure out the numnber, and connect e.g. to `http://18.3.4.5:8000/`
|
||||
## Running Tests
|
||||
|
||||
See `testing.md` for instructions on running the test suite.
|
||||
|
||||
## Content development
|
||||
|
||||
|
||||
@@ -387,7 +387,14 @@ Inherited
|
||||
When this content should be shown to students. Note that anyone with staff access to the course will always see everything.
|
||||
|
||||
`showanswer`
|
||||
When to show answer. For 'attempted', will show answer after first attempt. Values: never, attempted, answered, closed. Default: closed. Optional.
|
||||
When to show answer. Values: never, attempted, answered, closed, finished, past_due, always. Default: closed. Optional.
|
||||
- `never`: never show answer
|
||||
- `attempted`: show answer after first attempt
|
||||
- `answered` : this is slightly different from `attempted` -- resetting the problems makes "done" False, but leaves attempts unchanged.
|
||||
- `closed` : show answer after problem is closed, ie due date is past, or maximum attempts exceeded.
|
||||
- `finished` : show answer after problem closed, or is correctly answered.
|
||||
- `past_due` : show answer after problem due date is past.
|
||||
- `always` : always allow answer to be shown.
|
||||
|
||||
`graded`
|
||||
Whether this section will count towards the students grade. "true" or "false". Defaults to "false".
|
||||
|
||||
BIN
doc/test_pyramid.png
Normal file
BIN
doc/test_pyramid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
236
doc/testing.md
236
doc/testing.md
@@ -1,68 +1,218 @@
|
||||
# Testing
|
||||
|
||||
Testing is good. Here is some useful info about how we set up tests.
|
||||
More info is [on the wiki](https://edx-wiki.atlassian.net/wiki/display/ENG/Test+Engineering)
|
||||
## Overview
|
||||
|
||||
## Backend code
|
||||
We maintain three kinds of tests: unit tests, integration tests,
|
||||
and acceptance tests.
|
||||
|
||||
- The python unit tests can be run via rake tasks.
|
||||
See development.md for more info on how to do this.
|
||||
### Unit Tests
|
||||
|
||||
## Frontend code
|
||||
* Each test case should be concise: setup, execute, check, and teardown.
|
||||
If you find yourself writing tests with many steps, consider refactoring
|
||||
the unit under tests into smaller units, and then testing those individually.
|
||||
|
||||
### Jasmine
|
||||
* As a rule of thumb, your unit tests should cover every code branch.
|
||||
|
||||
We're using Jasmine to unit/integration test the JavaScript files.
|
||||
More info [on the wiki](https://edx-wiki.atlassian.net/wiki/display/ENG/Jasmine)
|
||||
* Mock or patch external dependencies.
|
||||
We use [voidspace mock](http://www.voidspace.org.uk/python/mock/).
|
||||
|
||||
All the specs are written in CoffeeScript to be consistent with the code.
|
||||
To access the test cases, start the server using the settings file **jasmine.py** using this command:
|
||||
`rake django-admin[runserver,lms,jasmine,12345]`
|
||||
* We unit test Python code (using [unittest](http://docs.python.org/2/library/unittest.html)) and
|
||||
Javascript (using [Jasmine](http://pivotal.github.io/jasmine/))
|
||||
|
||||
Then navigate to `http://localhost:12345/_jasmine/` to see the test results.
|
||||
### Integration Tests
|
||||
* Test several units at the same time.
|
||||
Note that you can still mock or patch dependencies
|
||||
that are not under test! For example, you might test that
|
||||
`LoncapaProblem`, `NumericalResponse`, and `CorrectMap` in the
|
||||
`capa` package work together, while still mocking out template rendering.
|
||||
|
||||
All the JavaScript codes must have test coverage. Both CMS and LMS
|
||||
has its own test directory in `{cms,lms}/static/coffee/spec` If you haven't
|
||||
written a JavaScript test before, you can look at those example files as a
|
||||
starting point. Also, these materials might be helpful for you:
|
||||
* Use integration tests to ensure that units are hooked up correctly.
|
||||
You do not need to test every possible input--that's what unit
|
||||
tests are for. Instead, focus on testing the "happy path"
|
||||
to verify that the components work together correctly.
|
||||
|
||||
CMS Note: For consistency, you're advised to use the same directory structure
|
||||
for implementation and test. For example, test for `src/views/module.coffee`
|
||||
* Many of our tests use the [Django test client](https://docs.djangoproject.com/en/dev/topics/testing/overview/) to simulate
|
||||
HTTP requests to the server.
|
||||
|
||||
### UI Acceptance Tests
|
||||
* Use these to test that major program features are working correctly.
|
||||
|
||||
* We use [lettuce](http://lettuce.it/) to write BDD-style tests. Most of
|
||||
these tests simulate user interactions through the browser using
|
||||
[splinter](http://splinter.cobrateam.info/).
|
||||
|
||||
Overall, you want to write the tests that **maximize coverage**
|
||||
while **minimizing maintenance**.
|
||||
In practice, this usually means investing heavily
|
||||
in unit tests, which tend to be the most robust to changes in the code base.
|
||||
|
||||

|
||||
|
||||
The pyramid above shows the relative number of unit tests, integration tests,
|
||||
and acceptance tests. Most of our tests are unit tests or integration tests.
|
||||
|
||||
## Test Locations
|
||||
|
||||
* Python unit and integration tests: Located in
|
||||
subpackages called `tests`.
|
||||
For example, the tests for the `capa` package are located in
|
||||
`common/lib/capa/capa/tests`.
|
||||
|
||||
* Javascript unit tests: Located in `spec` folders. For example,
|
||||
`common/lib/xmodule/xmodule/js/spec` and `{cms,lms}/static/coffee/spec`
|
||||
For consistency, you should use the same directory structure for implementation
|
||||
and test. For example, the test for `src/views/module.coffee`
|
||||
should be written in `spec/views/module_spec.coffee`.
|
||||
|
||||
* http://pivotal.github.com/jasmine
|
||||
* http://railscasts.com/episodes/261-testing-javascript-with-jasmine?view=asciicast
|
||||
* http://a-developer-life.blogspot.com/2011/05/jasmine-part-1-unit-testing-javascript.html
|
||||
* UI acceptance tests:
|
||||
- Set up and helper methods: `common/djangoapps/terrain`
|
||||
- Tests: located in `features` subpackage within a Django app.
|
||||
For example: `lms/djangoapps/courseware/features`
|
||||
|
||||
If you're finishing a feature that contains JavaScript code snippets and do not
|
||||
sure how to test, please feel free to open up a pull request and asking people
|
||||
for help. (However, the best way to do it would be writing your test first, then
|
||||
implement your feature - Test Driven Development.)
|
||||
|
||||
### BDD style acceptance tests with Lettuce
|
||||
## Factories
|
||||
|
||||
We're using Lettuce for end user acceptance testing of features.
|
||||
More info [on the wiki](https://edx-wiki.atlassian.net/wiki/display/ENG/Lettuce+Acceptance+Testing)
|
||||
Many tests delegate set-up to a "factory" class. For example,
|
||||
there are factories for creating courses, problems, and users.
|
||||
This encapsulates set-up logic from tests.
|
||||
|
||||
Lettuce is a port of Cucumber. We're using it to drive Splinter, which is a python wrapper to Selenium.
|
||||
To execute the automated test scripts, you'll need to start up the django server separately, then launch the tests.
|
||||
Do both use the settings file named **acceptance.py**.
|
||||
Factories are often implemented using [FactoryBoy](https://readthedocs.org/projects/factoryboy/)
|
||||
|
||||
What this will do is to use a sqllite database named mitx_all/db/test_mitx.db.
|
||||
That way it can be flushed etc. without messing up your dev db.
|
||||
Note that this also means that you need to syncdb and migrate the db first before starting the server to initialize it if it does not yet exist.
|
||||
In general, factories should be located close to the code they use.
|
||||
For example, the factory for creating problem XML definitions
|
||||
is located in `common/lib/capa/capa/tests/response_xml_factory.py`
|
||||
because the `capa` package handles problem XML.
|
||||
|
||||
|
||||
# Running Tests
|
||||
|
||||
Before running tests, ensure that you have all the dependencies. You can install dependencies using:
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
|
||||
## Running Python Unit tests
|
||||
|
||||
We use [nose](https://nose.readthedocs.org/en/latest/) through
|
||||
the [django-nose plugin](https://pypi.python.org/pypi/django-nose)
|
||||
to run the test suite.
|
||||
|
||||
You can run tests using `rake` commands. For example,
|
||||
|
||||
rake test
|
||||
|
||||
runs all the tests. It also runs `collectstatic`, which prepares the static files used by the site (for example, compiling Coffeescript to Javascript).
|
||||
|
||||
You can also run the tests without `collectstatic`, which tends to be faster:
|
||||
|
||||
rake fasttest_lms
|
||||
|
||||
or
|
||||
|
||||
rake fasttest_cms
|
||||
|
||||
xmodule can be tested independently, with this:
|
||||
|
||||
rake test_common/lib/xmodule
|
||||
|
||||
To run a single django test class:
|
||||
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth
|
||||
|
||||
To run a single django test:
|
||||
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth.test_dark_launch
|
||||
|
||||
|
||||
To run a single nose test file:
|
||||
|
||||
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py
|
||||
|
||||
To run a single nose test:
|
||||
|
||||
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify
|
||||
|
||||
|
||||
Very handy: if you uncomment the `pdb=1` line in `setup.cfg`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out [the pdb documentation](http://docs.python.org/library/pdb.html)
|
||||
|
||||
### Running Javascript Unit Tests
|
||||
|
||||
These commands start a development server with jasmine testing enabled, and launch your default browser
|
||||
pointing to those tests
|
||||
|
||||
rake browse_jasmine_{lms,cms}
|
||||
|
||||
To run the tests headless, you must install [phantomjs](http://phantomjs.org/download.html), then run:
|
||||
|
||||
rake phantomjs_jasmine_{lms,cms}
|
||||
|
||||
If the `phantomjs` binary is not on the path, set the `PHANTOMJS_PATH` environment variable to point to it
|
||||
|
||||
PHANTOMJS_PATH=/path/to/phantomjs rake phantomjs_jasmine_{lms,cms}
|
||||
|
||||
Once you have run the `rake` command, your browser should open to
|
||||
to `http://localhost/_jasmine/`, which displays the test results.
|
||||
|
||||
**Troubleshooting**: If you get an error message while running the `rake` task,
|
||||
try running `bundle install` to install the required ruby gems.
|
||||
|
||||
### Running Acceptance Tests
|
||||
|
||||
We use [Lettuce](http://lettuce.it/) for acceptance testing.
|
||||
Most of our tests use [Splinter](http://splinter.cobrateam.info/)
|
||||
to simulate UI browser interactions. Splinter, in turn,
|
||||
uses [Selenium](http://docs.seleniumhq.org/) to control the browser.
|
||||
|
||||
**Prerequisite**: You must have [ChromeDriver](https://code.google.com/p/selenium/wiki/ChromeDriver)
|
||||
installed to run the tests in Chrome.
|
||||
|
||||
Before running the tests, you need to set up the test database:
|
||||
|
||||
1. Set up the test database (only needs to be done once):
|
||||
rm ../db/test_mitx.db
|
||||
rake django-admin[syncdb,lms,acceptance,--noinput]
|
||||
rake django-admin[migrate,lms,acceptance,--noinput]
|
||||
|
||||
2. Start up the django server separately in a shell
|
||||
rake lms[acceptance]
|
||||
To run the acceptance tests:
|
||||
|
||||
3. Then in another shell, run the tests in different ways as below. Lettuce comes with a new django-admin command called _harvest_. See the [lettuce django docs](http://lettuce.it/recipes/django-lxml.html) for more details.
|
||||
* All tests in a specified feature folder: `django-admin.py harvest --no-server --settings=lms.envs.acceptance --pythonpath=. lms/djangoapps/portal/features/`
|
||||
* Only the specified feature's scenarios: `django-admin.py harvest --no-server --settings=lms.envs.acceptance --pythonpath=. lms/djangoapps/courseware/features/high-level-tabs.feature`
|
||||
1. Start the Django server locally using the settings in **acceptance.py**:
|
||||
|
||||
4. Troubleshooting
|
||||
* If you get an error msg that says something about harvest not being a command, you probably are missing a requirement. Pip install (test-requirements.txt) and/or brew install as needed.
|
||||
rake lms[acceptance]
|
||||
|
||||
2. In another shell, run the tests:
|
||||
|
||||
django-admin.py harvest --no-server --settings=lms.envs.acceptance --pythonpath=. lms/djangoapps/portal/features/
|
||||
|
||||
To test only a specific feature:
|
||||
|
||||
django-admin.py harvest --no-server --settings=lms.envs.acceptance --pythonpath=. lms/djangoapps/courseware/features/high-level-tabs.feature
|
||||
|
||||
**Troubleshooting**: If you get an error message that says something about harvest not being a command, you probably are missing a requirement.
|
||||
Try running:
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
|
||||
## Viewing Test Coverage
|
||||
|
||||
We currently collect test coverage information for Python unit/integration tests.
|
||||
|
||||
To view test coverage:
|
||||
|
||||
1. Run the test suite:
|
||||
|
||||
rake test
|
||||
|
||||
2. Generate reports:
|
||||
|
||||
rake coverage:html
|
||||
|
||||
3. HTML reports are located in the `reports` folder.
|
||||
|
||||
|
||||
## Testing using queue servers
|
||||
|
||||
When testing problems that use a queue server on AWS (e.g. sandbox-xqueue.edx.org), you'll need to run your server on your public IP, like so.
|
||||
|
||||
`django-admin.py runserver --settings=lms.envs.dev --pythonpath=. 0.0.0.0:8000`
|
||||
|
||||
When you connect to the LMS, you need to use the public ip. Use `ifconfig` to figure out the number, and connect e.g. to `http://18.3.4.5:8000/`
|
||||
|
||||
@@ -15,6 +15,7 @@ from xmodule.modulestore import Location
|
||||
from xmodule.x_module import XModule, XModuleDescriptor
|
||||
|
||||
from student.models import CourseEnrollmentAllowed
|
||||
from courseware.masquerade import is_masquerading_as_student
|
||||
|
||||
DEBUG_ACCESS = False
|
||||
|
||||
@@ -235,7 +236,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None):
|
||||
don't have to hit the enrollments table on every module load.
|
||||
"""
|
||||
# If start dates are off, can always load
|
||||
if settings.MITX_FEATURES['DISABLE_START_DATES']:
|
||||
if settings.MITX_FEATURES['DISABLE_START_DATES'] and not is_masquerading_as_student(user):
|
||||
debug("Allow: DISABLE_START_DATES")
|
||||
return True
|
||||
|
||||
@@ -543,6 +544,10 @@ def _has_access_to_location(user, location, access_level, course_context):
|
||||
if user is None or (not user.is_authenticated()):
|
||||
debug("Deny: no user or anon user")
|
||||
return False
|
||||
|
||||
if is_masquerading_as_student(user):
|
||||
return False
|
||||
|
||||
if user.is_staff:
|
||||
debug("Allow: user.is_staff")
|
||||
return True
|
||||
|
||||
@@ -15,6 +15,7 @@ Feature: Answer problems
|
||||
| drop down |
|
||||
| multiple choice |
|
||||
| checkbox |
|
||||
| radio |
|
||||
| string |
|
||||
| numerical |
|
||||
| formula |
|
||||
@@ -33,6 +34,7 @@ Feature: Answer problems
|
||||
| drop down |
|
||||
| multiple choice |
|
||||
| checkbox |
|
||||
| radio |
|
||||
| string |
|
||||
| numerical |
|
||||
| formula |
|
||||
@@ -50,6 +52,7 @@ Feature: Answer problems
|
||||
| drop down |
|
||||
| multiple choice |
|
||||
| checkbox |
|
||||
| radio |
|
||||
| string |
|
||||
| numerical |
|
||||
| formula |
|
||||
@@ -71,6 +74,8 @@ Feature: Answer problems
|
||||
| multiple choice | incorrect |
|
||||
| checkbox | correct |
|
||||
| checkbox | incorrect |
|
||||
| radio | correct |
|
||||
| radio | incorrect |
|
||||
| string | correct |
|
||||
| string | incorrect |
|
||||
| numerical | correct |
|
||||
|
||||
@@ -42,7 +42,13 @@ PROBLEM_FACTORY_DICT = {
|
||||
'choice_type': 'checkbox',
|
||||
'choices': [True, False, True, False, False],
|
||||
'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}},
|
||||
|
||||
'radio': {
|
||||
'factory': ChoiceResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'The correct answer is Choice 3',
|
||||
'choice_type': 'radio',
|
||||
'choices': [False, False, True, False],
|
||||
'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}},
|
||||
'string': {
|
||||
'factory': StringResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
@@ -174,6 +180,12 @@ def answer_problem(step, problem_type, correctness):
|
||||
else:
|
||||
inputfield('checkbox', choice='choice_3').check()
|
||||
|
||||
elif problem_type == 'radio':
|
||||
if correctness == 'correct':
|
||||
inputfield('radio', choice='choice_2').check()
|
||||
else:
|
||||
inputfield('radio', choice='choice_1').check()
|
||||
|
||||
elif problem_type == 'string':
|
||||
textvalue = 'correct string' if correctness == 'correct' \
|
||||
else 'incorrect'
|
||||
@@ -252,6 +264,14 @@ def assert_problem_has_answer(step, problem_type, answer_class):
|
||||
else:
|
||||
assert_checked('checkbox', [])
|
||||
|
||||
elif problem_type == "radio":
|
||||
if answer_class == 'correct':
|
||||
assert_checked('radio', ['choice_2'])
|
||||
elif answer_class == 'incorrect':
|
||||
assert_checked('radio', ['choice_1'])
|
||||
else:
|
||||
assert_checked('radio', [])
|
||||
|
||||
elif problem_type == 'string':
|
||||
if answer_class == 'blank':
|
||||
expected = ''
|
||||
@@ -298,6 +318,7 @@ CORRECTNESS_SELECTORS = {
|
||||
'correct': {'drop down': ['span.correct'],
|
||||
'multiple choice': ['label.choicegroup_correct'],
|
||||
'checkbox': ['span.correct'],
|
||||
'radio': ['label.choicegroup_correct'],
|
||||
'string': ['div.correct'],
|
||||
'numerical': ['div.correct'],
|
||||
'formula': ['div.correct'],
|
||||
@@ -308,6 +329,8 @@ CORRECTNESS_SELECTORS = {
|
||||
'multiple choice': ['label.choicegroup_incorrect',
|
||||
'span.incorrect'],
|
||||
'checkbox': ['span.incorrect'],
|
||||
'radio': ['label.choicegroup_incorrect',
|
||||
'span.incorrect'],
|
||||
'string': ['div.incorrect'],
|
||||
'numerical': ['div.incorrect'],
|
||||
'formula': ['div.incorrect'],
|
||||
@@ -317,6 +340,7 @@ CORRECTNESS_SELECTORS = {
|
||||
'unanswered': {'drop down': ['span.unanswered'],
|
||||
'multiple choice': ['span.unanswered'],
|
||||
'checkbox': ['span.unanswered'],
|
||||
'radio': ['span.unanswered'],
|
||||
'string': ['div.unanswered'],
|
||||
'numerical': ['div.unanswered'],
|
||||
'formula': ['div.unanswered'],
|
||||
|
||||
65
lms/djangoapps/courseware/masquerade.py
Normal file
65
lms/djangoapps/courseware/masquerade.py
Normal file
@@ -0,0 +1,65 @@
|
||||
'''
|
||||
---------------------------------------- Masequerade ----------------------------------------
|
||||
Allow course staff to see a student or staff view of courseware.
|
||||
Which kind of view has been selected is stored in the session state.
|
||||
'''
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MASQ_KEY = 'masquerade_identity'
|
||||
|
||||
|
||||
def handle_ajax(request, marg):
|
||||
'''
|
||||
Handle ajax call from "staff view" / "student view" toggle button
|
||||
'''
|
||||
if marg == 'toggle':
|
||||
status = request.session.get(MASQ_KEY, '')
|
||||
if status is None or status in ['', 'staff']:
|
||||
status = 'student'
|
||||
else:
|
||||
status = 'staff'
|
||||
request.session[MASQ_KEY] = status
|
||||
return HttpResponse(json.dumps({'status': status}))
|
||||
|
||||
|
||||
def setup_masquerade(request, staff_access=False):
|
||||
'''
|
||||
Setup masquerade identity (allows staff to view courseware as either staff or student)
|
||||
|
||||
Uses request.session[MASQ_KEY] to store status of masquerading.
|
||||
Adds masquerade status to request.user, if masquerading active.
|
||||
Return string version of status of view (either 'staff' or 'student')
|
||||
'''
|
||||
if request.user is None:
|
||||
return None
|
||||
|
||||
if not settings.MITX_FEATURES.get('ENABLE_MASQUERADE', False):
|
||||
return None
|
||||
|
||||
if not staff_access: # can masquerade only if user has staff access to course
|
||||
return None
|
||||
|
||||
usertype = request.session.get(MASQ_KEY, '')
|
||||
if usertype is None or not usertype:
|
||||
request.session[MASQ_KEY] = 'staff'
|
||||
usertype = 'staff'
|
||||
|
||||
if usertype == 'student':
|
||||
request.user.masquerade_as_student = True
|
||||
|
||||
return usertype
|
||||
|
||||
|
||||
def is_masquerading_as_student(user):
|
||||
'''
|
||||
Return True if user is masquerading as a student, False otherwise
|
||||
'''
|
||||
masq = getattr(user, 'masquerade_as_student', False)
|
||||
return masq==True
|
||||
@@ -17,6 +17,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
from courseware.masquerade import setup_masquerade
|
||||
from courseware.access import has_access
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from .models import StudentModule
|
||||
@@ -164,6 +165,10 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
Actually implement get_module. See docstring there for details.
|
||||
"""
|
||||
|
||||
# allow course staff to masquerade as student
|
||||
if has_access(user, descriptor, 'staff', course_id):
|
||||
setup_masquerade(request, True)
|
||||
|
||||
# Short circuit--if the user shouldn't have access, bail without doing any work
|
||||
if not has_access(user, descriptor, 'load', course_id):
|
||||
return None
|
||||
|
||||
@@ -1,62 +1,85 @@
|
||||
import factory
|
||||
from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed)
|
||||
from courseware.models import StudentModule
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
import json
|
||||
from functools import partial
|
||||
|
||||
from factory import DjangoModelFactory, SubFactory
|
||||
from student.tests.factories import UserFactory as StudentUserFactory
|
||||
from student.tests.factories import GroupFactory as StudentGroupFactory
|
||||
from student.tests.factories import UserProfileFactory as StudentUserProfileFactory
|
||||
from student.tests.factories import CourseEnrollmentAllowedFactory as StudentCourseEnrollmentAllowedFactory
|
||||
from student.tests.factories import RegistrationFactory as StudentRegistrationFactory
|
||||
from courseware.models import StudentModule, XModuleContentField, XModuleSettingsField
|
||||
from courseware.models import XModuleStudentInfoField, XModuleStudentPrefsField
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
|
||||
|
||||
|
||||
class UserProfileFactory(factory.Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
class UserProfileFactory(StudentUserProfileFactory):
|
||||
name = 'Robot Studio'
|
||||
courseware = 'course.xml'
|
||||
|
||||
|
||||
class RegistrationFactory(factory.Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid.uuid4().hex
|
||||
class RegistrationFactory(StudentRegistrationFactory):
|
||||
pass
|
||||
|
||||
|
||||
class UserFactory(factory.Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
class UserFactory(StudentUserFactory):
|
||||
email = 'robot@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Tester'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime.now()
|
||||
date_joined = datetime.now()
|
||||
|
||||
|
||||
class GroupFactory(factory.Factory):
|
||||
FACTORY_FOR = Group
|
||||
|
||||
class GroupFactory(StudentGroupFactory):
|
||||
name = 'test_group'
|
||||
|
||||
|
||||
class CourseEnrollmentAllowedFactory(factory.Factory):
|
||||
FACTORY_FOR = CourseEnrollmentAllowed
|
||||
|
||||
email = 'test@edx.org'
|
||||
course_id = 'edX/test/2012_Fall'
|
||||
class CourseEnrollmentAllowedFactory(StudentCourseEnrollmentAllowedFactory):
|
||||
pass
|
||||
|
||||
|
||||
class StudentModuleFactory(factory.Factory):
|
||||
class StudentModuleFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = StudentModule
|
||||
|
||||
module_type = "problem"
|
||||
student = factory.SubFactory(UserFactory)
|
||||
student = SubFactory(UserFactory)
|
||||
course_id = "MITx/999/Robot_Super_Course"
|
||||
state = None
|
||||
grade = None
|
||||
max_grade = None
|
||||
done = 'na'
|
||||
|
||||
|
||||
class ContentFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = XModuleContentField
|
||||
|
||||
field_name = 'existing_field'
|
||||
value = json.dumps('old_value')
|
||||
definition_id = location('def_id').url()
|
||||
|
||||
|
||||
class SettingsFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = XModuleSettingsField
|
||||
|
||||
field_name = 'existing_field'
|
||||
value = json.dumps('old_value')
|
||||
usage_id = '%s-%s' % ('edX/test_course/test', location('def_id').url())
|
||||
|
||||
|
||||
class StudentPrefsFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = XModuleStudentPrefsField
|
||||
|
||||
field_name = 'existing_field'
|
||||
value = json.dumps('old_value')
|
||||
student = SubFactory(UserFactory)
|
||||
module_type = 'problem'
|
||||
|
||||
|
||||
class StudentInfoFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = XModuleStudentInfoField
|
||||
|
||||
field_name = 'existing_field'
|
||||
value = json.dumps('old_value')
|
||||
student = SubFactory(UserFactory)
|
||||
|
||||
120
lms/djangoapps/courseware/tests/test_masquerade.py
Normal file
120
lms/djangoapps/courseware/tests/test_masquerade.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Unit tests for masquerade
|
||||
|
||||
Based on (and depends on) unit tests for courseware.
|
||||
|
||||
Notes for running by hand:
|
||||
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware
|
||||
"""
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
from courseware.access import _course_staff_group_name
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import xmodule.modulestore.django
|
||||
import json
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase):
|
||||
'''
|
||||
Check for staff being able to masquerade as student
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
|
||||
#self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
|
||||
#self.toy = modulestore().get_course("edX/toy/2012_Fall")
|
||||
self.graded_course = modulestore().get_course("edX/graded/2012_Fall")
|
||||
|
||||
# Create staff account
|
||||
self.instructor = 'view2@test.com'
|
||||
self.password = 'foo'
|
||||
self.create_account('u2', self.instructor, self.password)
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
def make_instructor(course):
|
||||
group_name = _course_staff_group_name(course.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(get_user(self.instructor))
|
||||
|
||||
make_instructor(self.graded_course)
|
||||
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
self.enroll(self.graded_course)
|
||||
# self.factory = RequestFactory()
|
||||
|
||||
def get_cw_section(self):
|
||||
url = reverse('courseware_section',
|
||||
kwargs={'course_id': self.graded_course.id,
|
||||
'chapter': 'GradedChapter',
|
||||
'section': 'Homework1'})
|
||||
|
||||
resp = self.client.get(url)
|
||||
|
||||
print "url ", url
|
||||
return resp
|
||||
|
||||
def test_staff_debug_for_staff(self):
|
||||
resp = self.get_cw_section()
|
||||
sdebug = '<div><a href="#i4x_edX_graded_problem_H1P1_debug" id="i4x_edX_graded_problem_H1P1_trig">Staff Debug Info</a></div>'
|
||||
|
||||
self.assertTrue(sdebug in resp.content)
|
||||
|
||||
|
||||
def toggle_masquerade(self):
|
||||
'''
|
||||
Toggle masquerade state
|
||||
'''
|
||||
masq_url = reverse('masquerade-switch', kwargs={'marg': 'toggle'})
|
||||
print "masq_url ", masq_url
|
||||
resp = self.client.get(masq_url)
|
||||
return resp
|
||||
|
||||
def test_no_staff_debug_for_student(self):
|
||||
togresp = self.toggle_masquerade()
|
||||
print "masq now ", togresp.content
|
||||
self.assertEqual(togresp.content, '{"status": "student"}', '')
|
||||
|
||||
resp = self.get_cw_section()
|
||||
sdebug = '<div><a href="#i4x_edX_graded_problem_H1P1_debug" id="i4x_edX_graded_problem_H1P1_trig">Staff Debug Info</a></div>'
|
||||
|
||||
self.assertFalse(sdebug in resp.content)
|
||||
|
||||
def get_problem(self):
|
||||
pun = 'H1P1'
|
||||
problem_location = "i4x://edX/graded/problem/%s" % pun
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
kwargs={'course_id': self.graded_course.id,
|
||||
'location': problem_location,
|
||||
'dispatch': 'problem_get', })
|
||||
|
||||
resp = self.client.get(modx_url)
|
||||
|
||||
print "modx_url ", modx_url
|
||||
return resp
|
||||
|
||||
def test_showanswer_for_staff(self):
|
||||
resp = self.get_problem()
|
||||
html = json.loads(resp.content)['html']
|
||||
print html
|
||||
sabut = '<input class="show" type="button" value="Show Answer">'
|
||||
self.assertTrue(sabut in html)
|
||||
|
||||
def test_no_showanswer_for_student(self):
|
||||
togresp = self.toggle_masquerade()
|
||||
print "masq now ", togresp.content
|
||||
self.assertEqual(togresp.content, '{"status": "student"}', '')
|
||||
|
||||
resp = self.get_problem()
|
||||
html = json.loads(resp.content)['html']
|
||||
print html
|
||||
sabut = '<input class="show" type="button" value="Show Answer">'
|
||||
self.assertFalse(sabut in html)
|
||||
@@ -1,15 +1,19 @@
|
||||
import factory
|
||||
import json
|
||||
from mock import Mock
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from functools import partial
|
||||
|
||||
from courseware.model_data import LmsKeyValueStore, InvalidWriteError, InvalidScopeError, ModelDataCache
|
||||
from courseware.models import StudentModule, XModuleContentField, XModuleSettingsField, XModuleStudentInfoField, XModuleStudentPrefsField
|
||||
from courseware.model_data import LmsKeyValueStore, InvalidWriteError
|
||||
from courseware.model_data import InvalidScopeError, ModelDataCache
|
||||
from courseware.models import StudentModule, XModuleContentField, XModuleSettingsField
|
||||
from courseware.models import XModuleStudentInfoField, XModuleStudentPrefsField
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from courseware.tests.factories import StudentModuleFactory as cmfStudentModuleFactory
|
||||
from courseware.tests.factories import ContentFactory, SettingsFactory
|
||||
from courseware.tests.factories import StudentPrefsFactory, StudentInfoFactory
|
||||
|
||||
from xblock.core import Scope, BlockScope
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
@@ -19,6 +23,7 @@ def mock_field(scope, name):
|
||||
field.name = name
|
||||
return field
|
||||
|
||||
|
||||
def mock_descriptor(fields=[], lms_fields=[]):
|
||||
descriptor = Mock()
|
||||
descriptor.stores_state = True
|
||||
@@ -37,53 +42,9 @@ prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'problem')
|
||||
user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None)
|
||||
|
||||
|
||||
class UserFactory(factory.Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'user'
|
||||
|
||||
|
||||
class StudentModuleFactory(factory.Factory):
|
||||
FACTORY_FOR = StudentModule
|
||||
|
||||
module_type = 'problem'
|
||||
class StudentModuleFactory(cmfStudentModuleFactory):
|
||||
module_state_key = location('def_id').url()
|
||||
student = factory.SubFactory(UserFactory)
|
||||
course_id = course_id
|
||||
state = None
|
||||
|
||||
|
||||
class ContentFactory(factory.Factory):
|
||||
FACTORY_FOR = XModuleContentField
|
||||
|
||||
field_name = 'existing_field'
|
||||
value = json.dumps('old_value')
|
||||
definition_id = location('def_id').url()
|
||||
|
||||
|
||||
class SettingsFactory(factory.Factory):
|
||||
FACTORY_FOR = XModuleSettingsField
|
||||
|
||||
field_name = 'existing_field'
|
||||
value = json.dumps('old_value')
|
||||
usage_id = '%s-%s' % (course_id, location('def_id').url())
|
||||
|
||||
|
||||
class StudentPrefsFactory(factory.Factory):
|
||||
FACTORY_FOR = XModuleStudentPrefsField
|
||||
|
||||
field_name = 'existing_field'
|
||||
value = json.dumps('old_value')
|
||||
student = factory.SubFactory(UserFactory)
|
||||
module_type = 'problem'
|
||||
|
||||
|
||||
class StudentInfoFactory(factory.Factory):
|
||||
FACTORY_FOR = XModuleStudentInfoField
|
||||
|
||||
field_name = 'existing_field'
|
||||
value = json.dumps('old_value')
|
||||
student = factory.SubFactory(UserFactory)
|
||||
|
||||
|
||||
class TestDescriptorFallback(TestCase):
|
||||
@@ -114,7 +75,7 @@ class TestDescriptorFallback(TestCase):
|
||||
class TestInvalidScopes(TestCase):
|
||||
def setUp(self):
|
||||
self.desc_md = {}
|
||||
self.user = UserFactory.create()
|
||||
self.user = UserFactory.create(username='user')
|
||||
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user)
|
||||
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
|
||||
|
||||
@@ -180,7 +141,7 @@ class TestStudentModuleStorage(TestCase):
|
||||
|
||||
class TestMissingStudentModule(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create()
|
||||
self.user = UserFactory.create(username='user')
|
||||
self.desc_md = {}
|
||||
self.mdc = ModelDataCache([mock_descriptor()], course_id, self.user)
|
||||
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
|
||||
|
||||
@@ -20,6 +20,7 @@ from courseware.access import has_access
|
||||
from courseware.courses import (get_courses, get_course_with_access,
|
||||
get_courses_by_university, sort_by_announcement)
|
||||
import courseware.tabs as tabs
|
||||
from courseware.masquerade import setup_masquerade
|
||||
from courseware.model_data import ModelDataCache
|
||||
from .module_render import toc_for_course, get_module_for_descriptor, get_module
|
||||
from courseware.models import StudentModule, StudentModuleHistory
|
||||
@@ -89,6 +90,7 @@ def render_accordion(request, course, chapter, section, model_data_cache):
|
||||
|
||||
# grab the table of contents
|
||||
user = User.objects.prefetch_related("groups").get(id=request.user.id)
|
||||
request.user = user # keep just one instance of User
|
||||
toc = toc_for_course(user, request, course, chapter, section, model_data_cache)
|
||||
|
||||
context = dict([('toc', toc),
|
||||
@@ -260,6 +262,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
- HTTPresponse
|
||||
"""
|
||||
user = User.objects.prefetch_related("groups").get(id=request.user.id)
|
||||
request.user = user # keep just one instance of User
|
||||
course = get_course_with_access(user, course_id, 'load', depth=2)
|
||||
staff_access = has_access(user, course, 'staff')
|
||||
registered = registered_for_course(course, user)
|
||||
@@ -268,6 +271,8 @@ def index(request, course_id, chapter=None, section=None,
|
||||
log.debug('User %s tried to view course %s but is not enrolled' % (user, course.location.url()))
|
||||
return redirect(reverse('about_course', args=[course.id]))
|
||||
|
||||
masq = setup_masquerade(request, staff_access)
|
||||
|
||||
try:
|
||||
model_data_cache = ModelDataCache.cache_for_descriptor_descendents(
|
||||
course.id, user, course, depth=2)
|
||||
@@ -289,6 +294,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
'init': '',
|
||||
'content': '',
|
||||
'staff_access': staff_access,
|
||||
'masquerade': masq,
|
||||
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa')
|
||||
}
|
||||
|
||||
@@ -301,12 +307,18 @@ def index(request, course_id, chapter=None, section=None,
|
||||
chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter)
|
||||
if chapter_module is None:
|
||||
# User may be trying to access a chapter that isn't live yet
|
||||
if masq=='student': # if staff is masquerading as student be kinder, don't 404
|
||||
log.debug('staff masq as student: no chapter %s' % chapter)
|
||||
return redirect(reverse('courseware', args=[course.id]))
|
||||
raise Http404
|
||||
|
||||
if section is not None:
|
||||
section_descriptor = chapter_descriptor.get_child_by(lambda m: m.url_name == section)
|
||||
if section_descriptor is None:
|
||||
# Specifically asked-for section doesn't exist
|
||||
if masq=='student': # if staff is masquerading as student be kinder, don't 404
|
||||
log.debug('staff masq as student: no section %s' % section)
|
||||
return redirect(reverse('courseware', args=[course.id]))
|
||||
raise Http404
|
||||
|
||||
# cdodge: this looks silly, but let's refetch the section_descriptor with depth=None
|
||||
@@ -437,9 +449,10 @@ def course_info(request, course_id):
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page
|
||||
|
||||
return render_to_response('courseware/info.html', {'request': request, 'course_id': course_id, 'cache': None,
|
||||
'course': course, 'staff_access': staff_access})
|
||||
'course': course, 'staff_access': staff_access, 'masquerade': masq})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
|
||||
@@ -11,6 +11,8 @@ urlpatterns = patterns('django_comment_client.base.views',
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/delete', 'delete_thread', name='delete_thread'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/upvote$', 'vote_for_thread', {'value': 'up'}, name='upvote_thread'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/downvote$', 'vote_for_thread', {'value': 'down'}, name='downvote_thread'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/flagAbuse$', 'flag_abuse_for_thread', name='flag_abuse_for_thread'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/unFlagAbuse$', 'un_flag_abuse_for_thread', name='un_flag_abuse_for_thread'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/unvote$', 'undo_vote_for_thread', name='undo_vote_for_thread'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/pin$', 'pin_thread', name='pin_thread'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/unpin$', 'un_pin_thread', name='un_pin_thread'),
|
||||
@@ -25,7 +27,8 @@ urlpatterns = patterns('django_comment_client.base.views',
|
||||
url(r'comments/(?P<comment_id>[\w\-]+)/upvote$', 'vote_for_comment', {'value': 'up'}, name='upvote_comment'),
|
||||
url(r'comments/(?P<comment_id>[\w\-]+)/downvote$', 'vote_for_comment', {'value': 'down'}, name='downvote_comment'),
|
||||
url(r'comments/(?P<comment_id>[\w\-]+)/unvote$', 'undo_vote_for_comment', name='undo_vote_for_comment'),
|
||||
|
||||
url(r'comments/(?P<comment_id>[\w\-]+)/flagAbuse$', 'flag_abuse_for_comment', name='flag_abuse_for_comment'),
|
||||
url(r'comments/(?P<comment_id>[\w\-]+)/unFlagAbuse$', 'un_flag_abuse_for_comment', name='un_flag_abuse_for_comment'),
|
||||
url(r'^(?P<commentable_id>[\w\-.]+)/threads/create$', 'create_thread', name='create_thread'),
|
||||
# TODO should we search within the board?
|
||||
url(r'^(?P<commentable_id>[\w\-.]+)/threads/search_similar$', 'search_similar_threads', name='search_similar_threads'),
|
||||
|
||||
@@ -20,7 +20,7 @@ from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from courseware.courses import get_course_with_access
|
||||
from courseware.courses import get_course_with_access, get_course_by_id
|
||||
from course_groups.cohorts import get_cohort_id, is_commentable_cohorted
|
||||
|
||||
from django_comment_client.utils import JsonResponse, JsonError, extract, get_courseware_context
|
||||
@@ -119,7 +119,7 @@ def create_thread(request, course_id, commentable_id):
|
||||
#patch for backward compatibility to comments service
|
||||
if not 'pinned' in thread.attributes:
|
||||
thread['pinned'] = False
|
||||
|
||||
|
||||
if post.get('auto_subscribe', 'false').lower() == 'true':
|
||||
user = cc.User.from_django_user(request.user)
|
||||
user.follow(thread)
|
||||
@@ -284,6 +284,50 @@ def vote_for_thread(request, course_id, thread_id, value):
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def flag_abuse_for_thread(request, course_id, thread_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.flagAbuse(user, thread)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def un_flag_abuse_for_thread(request, course_id, thread_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
course = get_course_by_id(course_id)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff')
|
||||
thread.unFlagAbuse(user, thread, removeAll)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def flag_abuse_for_comment(request, course_id, comment_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.flagAbuse(user, comment)
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def un_flag_abuse_for_comment(request, course_id, comment_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
course = get_course_by_id(course_id)
|
||||
removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff')
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.unFlagAbuse(user, comment, removeAll)
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
@@ -293,19 +337,21 @@ def undo_vote_for_thread(request, course_id, thread_id):
|
||||
user.unvote(thread)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def pin_thread(request, course_id, thread_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.pin(user,thread_id)
|
||||
thread.pin(user, thread_id)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
def un_pin_thread(request, course_id, thread_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.un_pin(user,thread_id)
|
||||
thread.un_pin(user, thread_id)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@@ -452,16 +498,11 @@ def upload(request, course_id): # ajax upload file to a question or answer
|
||||
if not file_extension in cc_settings.ALLOWED_UPLOAD_FILE_TYPES:
|
||||
file_types = "', '".join(cc_settings.ALLOWED_UPLOAD_FILE_TYPES)
|
||||
msg = _("allowed file types are '%(file_types)s'") % \
|
||||
{'file_types': file_types}
|
||||
{'file_types': file_types}
|
||||
raise exceptions.PermissionDenied(msg)
|
||||
|
||||
# generate new file name
|
||||
new_file_name = str(
|
||||
time.time()
|
||||
).replace(
|
||||
'.',
|
||||
str(random.randint(0, 100000))
|
||||
) + file_extension
|
||||
new_file_name = str(time.time()).replace('.', str(random.randint(0, 100000))) + file_extension
|
||||
|
||||
file_storage = get_storage_class()()
|
||||
# use default storage to store file
|
||||
@@ -472,7 +513,7 @@ def upload(request, course_id): # ajax upload file to a question or answer
|
||||
if size > cc_settings.MAX_UPLOAD_FILE_SIZE:
|
||||
file_storage.delete(new_file_name)
|
||||
msg = _("maximum upload file size is %(file_size)sK") % \
|
||||
{'file_size': cc_settings.MAX_UPLOAD_FILE_SIZE}
|
||||
{'file_size': cc_settings.MAX_UPLOAD_FILE_SIZE}
|
||||
raise exceptions.PermissionDenied(msg)
|
||||
|
||||
except exceptions.PermissionDenied, e:
|
||||
|
||||
@@ -9,9 +9,10 @@ from django.contrib.auth.models import User
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from courseware.courses import get_course_with_access
|
||||
from course_groups.cohorts import (is_course_cohorted, get_cohort_id, is_commentable_cohorted,
|
||||
from course_groups.cohorts import (is_course_cohorted, get_cohort_id, is_commentable_cohorted,
|
||||
get_cohorted_commentables, get_course_cohorts, get_cohort_by_id)
|
||||
from courseware.access import has_access
|
||||
from django_comment_client.models import Role
|
||||
|
||||
from django_comment_client.permissions import cached_has_permission
|
||||
from django_comment_client.utils import (merge_dict, extract, strip_none, get_courseware_context)
|
||||
@@ -79,7 +80,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
|
||||
strip_none(extract(request.GET,
|
||||
['page', 'sort_key',
|
||||
'sort_order', 'text',
|
||||
'tags', 'commentable_ids'])))
|
||||
'tags', 'commentable_ids', 'flagged'])))
|
||||
|
||||
threads, page, num_pages = cc.Thread.search(query_params)
|
||||
|
||||
@@ -92,7 +93,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
|
||||
else:
|
||||
thread['group_name'] = ""
|
||||
thread['group_string'] = "This post visible to everyone."
|
||||
|
||||
|
||||
#patch for backward compatibility to comments service
|
||||
if not 'pinned' in thread:
|
||||
thread['pinned'] = False
|
||||
@@ -108,7 +109,6 @@ def inline_discussion(request, course_id, discussion_id):
|
||||
"""
|
||||
Renders JSON for DiscussionModules
|
||||
"""
|
||||
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
try:
|
||||
@@ -219,6 +219,7 @@ def forum_form_discussion(request, course_id):
|
||||
'threads': saxutils.escape(json.dumps(threads), escapedict),
|
||||
'thread_pages': query_params['num_pages'],
|
||||
'user_info': saxutils.escape(json.dumps(user_info), escapedict),
|
||||
'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, course, 'staff'),
|
||||
'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
|
||||
'course_id': course.id,
|
||||
'category_map': category_map,
|
||||
@@ -230,7 +231,6 @@ def forum_form_discussion(request, course_id):
|
||||
'is_course_cohorted': is_course_cohorted(course_id)
|
||||
}
|
||||
# print "start rendering.."
|
||||
|
||||
return render_to_response('discussion/index.html', context)
|
||||
|
||||
|
||||
@@ -242,19 +242,12 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
|
||||
try:
|
||||
thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id)
|
||||
|
||||
#patch for backward compatibility with comments service
|
||||
if not 'pinned' in thread.attributes:
|
||||
thread['pinned'] = False
|
||||
|
||||
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
|
||||
log.error("Error loading single thread.")
|
||||
raise Http404
|
||||
|
||||
if request.is_ajax():
|
||||
|
||||
courseware_context = get_courseware_context(thread, course)
|
||||
|
||||
annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info)
|
||||
context = {'thread': thread.to_dict(), 'course_id': course_id}
|
||||
# TODO: Remove completely or switch back to server side rendering
|
||||
@@ -326,6 +319,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
'thread_pages': query_params['num_pages'],
|
||||
'is_course_cohorted': is_course_cohorted(course_id),
|
||||
'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id),
|
||||
'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, course, 'staff'),
|
||||
'cohorts': cohorts,
|
||||
'user_cohort': get_cohort_id(request.user, course_id),
|
||||
'cohorted_commentables': cohorted_commentables
|
||||
@@ -413,7 +407,7 @@ def followed_threads(request, course_id, user_id):
|
||||
'user_info': saxutils.escape(json.dumps(user_info), escapedict),
|
||||
'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
|
||||
# 'content': content,
|
||||
}
|
||||
}
|
||||
|
||||
return render_to_response('discussion/user_profile.html', context)
|
||||
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError):
|
||||
|
||||
@@ -6,10 +6,11 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from django.contrib.auth.models import User
|
||||
import comment_client as cc
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Reload forum (comment client) users from existing users'
|
||||
|
||||
def adduser(self,user):
|
||||
def adduser(self, user):
|
||||
print user
|
||||
try:
|
||||
cc_user = cc.User.from_django_user(user)
|
||||
@@ -22,8 +23,7 @@ class Command(BaseCommand):
|
||||
uset = [User.objects.get(username=x) for x in args]
|
||||
else:
|
||||
uset = User.objects.all()
|
||||
|
||||
|
||||
for user in uset:
|
||||
self.adduser(user)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ def check_conditions_permissions(user, permissions, course_id, **kwargs):
|
||||
return True in results
|
||||
elif operator == "and":
|
||||
return not False in results
|
||||
|
||||
return test(user, permissions, operator="or")
|
||||
|
||||
|
||||
@@ -89,6 +88,10 @@ VIEW_PERMISSIONS = {
|
||||
'vote_for_comment' : [['vote', 'is_open']],
|
||||
'undo_vote_for_comment': [['unvote', 'is_open']],
|
||||
'vote_for_thread' : [['vote', 'is_open']],
|
||||
'flag_abuse_for_thread': [['vote', 'is_open']],
|
||||
'un_flag_abuse_for_thread': [['vote', 'is_open']],
|
||||
'flag_abuse_for_comment': [['vote', 'is_open']],
|
||||
'un_flag_abuse_for_comment': [['vote', 'is_open']],
|
||||
'undo_vote_for_thread': [['unvote', 'is_open']],
|
||||
'pin_thread': ['create_comment'],
|
||||
'un_pin_thread': ['create_comment'],
|
||||
|
||||
@@ -39,4 +39,3 @@ class CloseThreadTextTest(TestCase):
|
||||
self.assertEqual(mustache_helpers.close_thread_text(self.contentOpen), 'Close thread')
|
||||
|
||||
#########################################################################################
|
||||
|
||||
|
||||
@@ -1,42 +1,18 @@
|
||||
import string
|
||||
import random
|
||||
import collections
|
||||
|
||||
from django.test import TestCase
|
||||
from factory import DjangoModelFactory
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
|
||||
import factory
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import UserProfile, CourseEnrollment
|
||||
from django_comment_client.models import Role, Permission
|
||||
|
||||
import django_comment_client.models as models
|
||||
import django_comment_client.utils as utils
|
||||
|
||||
import xmodule.modulestore.django as django
|
||||
|
||||
|
||||
class UserFactory(factory.Factory):
|
||||
FACTORY_FOR = User
|
||||
username = 'robot'
|
||||
password = '123456'
|
||||
email = 'robot@edx.org'
|
||||
is_active = True
|
||||
is_staff = False
|
||||
|
||||
|
||||
class CourseEnrollmentFactory(factory.Factory):
|
||||
FACTORY_FOR = CourseEnrollment
|
||||
user = factory.SubFactory(UserFactory)
|
||||
course_id = 'edX/toy/2012_Fall'
|
||||
|
||||
|
||||
class RoleFactory(factory.Factory):
|
||||
class RoleFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = Role
|
||||
name = 'Student'
|
||||
course_id = 'edX/toy/2012_Fall'
|
||||
|
||||
|
||||
class PermissionFactory(factory.Factory):
|
||||
class PermissionFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = Permission
|
||||
name = 'create_comment'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import time
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
import time
|
||||
@@ -104,12 +105,12 @@ def filter_unstarted_categories(category_map):
|
||||
result_map = {}
|
||||
|
||||
unfiltered_queue = [category_map]
|
||||
filtered_queue = [result_map]
|
||||
filtered_queue = [result_map]
|
||||
|
||||
while len(unfiltered_queue) > 0:
|
||||
|
||||
unfiltered_map = unfiltered_queue.pop()
|
||||
filtered_map = filtered_queue.pop()
|
||||
filtered_map = filtered_queue.pop()
|
||||
|
||||
filtered_map["children"] = []
|
||||
filtered_map["entries"] = {}
|
||||
@@ -174,8 +175,7 @@ def initialize_discussion_info(course):
|
||||
category = " / ".join([x.strip() for x in category.split("/")])
|
||||
last_category = category.split("/")[-1]
|
||||
discussion_id_map[id] = {"location": module.location, "title": last_category + " / " + title}
|
||||
unexpanded_category_map[category].append({"title": title, "id": id,
|
||||
"sort_key": sort_key, "start_date": module.lms.start})
|
||||
unexpanded_category_map[category].append({"title": title, "id": id, "sort_key": sort_key, "start_date": module.lms.start})
|
||||
|
||||
category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)}
|
||||
for category_path, entries in unexpanded_category_map.items():
|
||||
@@ -202,9 +202,9 @@ def initialize_discussion_info(course):
|
||||
level = path[-1]
|
||||
if level not in node:
|
||||
node[level] = {"subcategories": defaultdict(dict),
|
||||
"entries": defaultdict(dict),
|
||||
"sort_key": level,
|
||||
"start_date": category_start_date}
|
||||
"entries": defaultdict(dict),
|
||||
"sort_key": level,
|
||||
"start_date": category_start_date}
|
||||
else:
|
||||
if node[level]["start_date"] > category_start_date:
|
||||
node[level]["start_date"] = category_start_date
|
||||
@@ -284,12 +284,12 @@ class QueryCountDebugMiddleware(object):
|
||||
|
||||
def get_ability(course_id, content, user):
|
||||
return {
|
||||
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"),
|
||||
'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"),
|
||||
'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if content['type'] == 'comment' else False,
|
||||
'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if content['type'] == 'thread' else "delete_comment"),
|
||||
'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if content['type'] == 'thread' else False,
|
||||
'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"),
|
||||
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"),
|
||||
'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"),
|
||||
'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if content['type'] == 'comment' else False,
|
||||
'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if content['type'] == 'thread' else "delete_comment"),
|
||||
'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if content['type'] == 'thread' else False,
|
||||
'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"),
|
||||
}
|
||||
|
||||
#TODO: RENAME
|
||||
@@ -318,6 +318,7 @@ def get_annotated_content_infos(course_id, thread, user, user_info):
|
||||
Get metadata for a thread and its children
|
||||
"""
|
||||
infos = {}
|
||||
|
||||
def annotate(content):
|
||||
infos[str(content['id'])] = get_annotated_content_info(course_id, content, user, user_info)
|
||||
for child in content.get('children', []):
|
||||
@@ -382,8 +383,8 @@ def get_courseware_context(content, course):
|
||||
location = id_map[id]["location"].url()
|
||||
title = id_map[id]["title"]
|
||||
|
||||
url = reverse('jump_to', kwargs={"course_id":course.location.course_id,
|
||||
"location": location})
|
||||
url = reverse('jump_to', kwargs={"course_id": course.location.course_id,
|
||||
"location": location})
|
||||
|
||||
content_info = {"courseware_url": url, "courseware_title": title}
|
||||
return content_info
|
||||
@@ -396,7 +397,8 @@ def safe_content(content):
|
||||
'updated_at', 'depth', 'type', 'commentable_id', 'comments_count',
|
||||
'at_position_list', 'children', 'highlighted_title', 'highlighted_body',
|
||||
'courseware_title', 'courseware_url', 'tags', 'unread_comments_count',
|
||||
'read', 'group_id', 'group_name', 'group_string', 'pinned'
|
||||
'read', 'group_id', 'group_name', 'group_string', 'pinned', 'abuse_flaggers'
|
||||
|
||||
]
|
||||
|
||||
if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False):
|
||||
|
||||
@@ -5,15 +5,19 @@ import json
|
||||
from uuid import uuid4
|
||||
from random import shuffle
|
||||
from tempfile import NamedTemporaryFile
|
||||
from factory import Factory, SubFactory
|
||||
from factory import DjangoModelFactory, SubFactory
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
from django.core.management import call_command
|
||||
from django.core.urlresolvers import reverse
|
||||
from nose.tools import assert_true
|
||||
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from licenses.models import CourseSoftware, UserLicense
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase, get_user
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
@@ -27,7 +31,7 @@ SERIAL_1 = '123456abcde'
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CourseSoftwareFactory(Factory):
|
||||
class CourseSoftwareFactory(DjangoModelFactory):
|
||||
'''Factory for generating CourseSoftware objects in database'''
|
||||
FACTORY_FOR = CourseSoftware
|
||||
|
||||
@@ -37,7 +41,7 @@ class CourseSoftwareFactory(Factory):
|
||||
course_id = COURSE_1
|
||||
|
||||
|
||||
class UserLicenseFactory(Factory):
|
||||
class UserLicenseFactory(DjangoModelFactory):
|
||||
'''
|
||||
Factory for generating UserLicense objects in database
|
||||
|
||||
@@ -46,19 +50,24 @@ class UserLicenseFactory(Factory):
|
||||
'''
|
||||
FACTORY_FOR = UserLicense
|
||||
|
||||
user = None
|
||||
software = SubFactory(CourseSoftwareFactory)
|
||||
serial = SERIAL_1
|
||||
|
||||
|
||||
class LicenseTestCase(LoginEnrollmentTestCase):
|
||||
class LicenseTestCase(TestCase):
|
||||
'''Tests for licenses.views'''
|
||||
def setUp(self):
|
||||
'''creates a user and logs in'''
|
||||
self.setup_viewtest_user()
|
||||
# self.setup_viewtest_user()
|
||||
self.user = UserFactory(username='test',
|
||||
email='test@edx.org', password='test_password')
|
||||
self.client = Client()
|
||||
assert_true(self.client.login(username='test', password='test_password'))
|
||||
self.software = CourseSoftwareFactory()
|
||||
|
||||
def test_get_license(self):
|
||||
UserLicenseFactory(user=get_user(self.viewtest_email), software=self.software)
|
||||
UserLicenseFactory(user=self.user, software=self.software)
|
||||
response = self.client.post(reverse('user_software_license'),
|
||||
{'software': SOFTWARE_1, 'generate': 'false'},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
@@ -125,7 +134,7 @@ class LicenseTestCase(LoginEnrollmentTestCase):
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
||||
def test_get_license_without_login(self):
|
||||
self.logout()
|
||||
self.client.logout()
|
||||
response = self.client.post(reverse('user_software_license'),
|
||||
{'software': SOFTWARE_1, 'generate': 'false'},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
|
||||
@@ -1,16 +1,61 @@
|
||||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_render(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
Render a normal page, like jobs
|
||||
"""
|
||||
self.assertEqual(1 + 1, 2)
|
||||
response = self.client.get("/jobs")
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
|
||||
def test_render_press_release(self):
|
||||
"""
|
||||
Render press releases from generic URL match
|
||||
"""
|
||||
# since I had to remap files, pedantically test all press releases
|
||||
# published to date. Decent positive test while we're at it.
|
||||
all_releases = ["/press/mit-and-harvard-announce-edx",
|
||||
"/press/uc-berkeley-joins-edx",
|
||||
"/press/edX-announces-proctored-exam-testing",
|
||||
"/press/elsevier-collaborates-with-edx",
|
||||
"/press/ut-joins-edx",
|
||||
"/press/cengage-to-provide-book-content",
|
||||
"/press/gates-foundation-announcement",
|
||||
"/press/wellesley-college-joins-edx",
|
||||
"/press/georgetown-joins-edx",
|
||||
"/press/spring-courses",
|
||||
"/press/lewin-course-announcement",
|
||||
"/press/bostonx-announcement",
|
||||
"/press/eric-lander-secret-of-life",
|
||||
"/press/edx-expands-internationally",
|
||||
"/press/xblock_announcement",
|
||||
"/press/stanford-to-work-with-edx",
|
||||
]
|
||||
|
||||
for rel in all_releases:
|
||||
response = self.client.get(rel)
|
||||
self.assertNotContains(response, "PAGE NOT FOUND", status_code=200)
|
||||
|
||||
# should work with caps
|
||||
response = self.client.get("/press/STANFORD-to-work-with-edx")
|
||||
self.assertContains(response, "Stanford", status_code=200)
|
||||
|
||||
# negative test
|
||||
response = self.client.get("/press/this-shouldnt-work")
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# can someone do something fishy? no.
|
||||
response = self.client.get("/press/../homework.html")
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# "." in is ascii 2E
|
||||
response = self.client.get("/press/%2E%2E/homework.html")
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
# security reasons.
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from mako.exceptions import TopLevelLookupException
|
||||
from django.shortcuts import redirect
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseNotFound, HttpResponseServerError
|
||||
from django.http import HttpResponseNotFound, HttpResponseServerError, Http404
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
|
||||
from util.cache import cache_if_anonymous
|
||||
@@ -40,6 +41,25 @@ def render(request, template):
|
||||
return render_to_response('static_templates/' + template, {})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def render_press_release(request, slug):
|
||||
"""
|
||||
Render a press release given a slug. Similar to the "render" function above,
|
||||
but takes a slug and does a basic conversion to convert it to a template file.
|
||||
a) all lower case,
|
||||
b) convert dashes to underscores, and
|
||||
c) appending ".html"
|
||||
"""
|
||||
template = slug.lower().replace('-', '_') + ".html"
|
||||
try:
|
||||
resp = render_to_response('static_templates/press_releases/' + template, {})
|
||||
except TopLevelLookupException:
|
||||
raise Http404
|
||||
else:
|
||||
return resp
|
||||
|
||||
|
||||
def render_404(request):
|
||||
return HttpResponseNotFound(render_to_string('static_templates/404.html', {}))
|
||||
|
||||
|
||||
@@ -67,3 +67,4 @@ MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True
|
||||
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
|
||||
INSTALLED_APPS += ('lettuce.django',)
|
||||
LETTUCE_APPS = ('courseware',)
|
||||
LETTUCE_BROWSER = 'chrome'
|
||||
|
||||
@@ -71,6 +71,8 @@ MITX_FEATURES = {
|
||||
'ENABLE_LMS_MIGRATION': False,
|
||||
'ENABLE_MANUAL_GIT_RELOAD': False,
|
||||
|
||||
'ENABLE_MASQUERADE': True, # allow course staff to change to student view of courseware
|
||||
|
||||
'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL
|
||||
|
||||
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
|
||||
|
||||
@@ -11,12 +11,12 @@ class Comment(models.Model):
|
||||
'id', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
|
||||
'endorsed', 'parent_id', 'thread_id', 'username', 'votes', 'user_id',
|
||||
'closed', 'created_at', 'updated_at', 'depth', 'at_position_list',
|
||||
'type', 'commentable_id',
|
||||
'type', 'commentable_id', 'abuse_flaggers'
|
||||
]
|
||||
|
||||
updatable_fields = [
|
||||
'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'closed',
|
||||
'user_id', 'endorsed',
|
||||
'user_id', 'endorsed'
|
||||
]
|
||||
|
||||
initializable_fields = updatable_fields
|
||||
@@ -42,6 +42,32 @@ class Comment(models.Model):
|
||||
else:
|
||||
return super(Comment, cls).url(action, params)
|
||||
|
||||
def flagAbuse(self, user, voteable):
|
||||
if voteable.type == 'thread':
|
||||
url = _url_for_flag_abuse_thread(voteable.id)
|
||||
elif voteable.type == 'comment':
|
||||
url = _url_for_flag_abuse_comment(voteable.id)
|
||||
else:
|
||||
raise CommentClientError("Can only flag/unflag threads or comments")
|
||||
params = {'user_id': user.id}
|
||||
request = perform_request('put', url, params)
|
||||
voteable.update_attributes(request)
|
||||
|
||||
def unFlagAbuse(self, user, voteable, removeAll):
|
||||
if voteable.type == 'thread':
|
||||
url = _url_for_unflag_abuse_thread(voteable.id)
|
||||
elif voteable.type == 'comment':
|
||||
url = _url_for_unflag_abuse_comment(voteable.id)
|
||||
else:
|
||||
raise CommentClientError("Can flag/unflag for threads or comments")
|
||||
params = {'user_id': user.id}
|
||||
|
||||
if removeAll:
|
||||
params['all'] = True
|
||||
|
||||
request = perform_request('put', url, params)
|
||||
voteable.update_attributes(request)
|
||||
|
||||
|
||||
def _url_for_thread_comments(thread_id):
|
||||
return "{prefix}/threads/{thread_id}/comments".format(prefix=settings.PREFIX, thread_id=thread_id)
|
||||
@@ -49,3 +75,11 @@ def _url_for_thread_comments(thread_id):
|
||||
|
||||
def _url_for_comment(comment_id):
|
||||
return "{prefix}/comments/{comment_id}".format(prefix=settings.PREFIX, comment_id=comment_id)
|
||||
|
||||
|
||||
def _url_for_flag_abuse_comment(comment_id):
|
||||
return "{prefix}/comments/{comment_id}/abuse_flag".format(prefix=settings.PREFIX, comment_id=comment_id)
|
||||
|
||||
|
||||
def _url_for_unflag_abuse_comment(comment_id):
|
||||
return "{prefix}/comments/{comment_id}/abuse_unflag".format(prefix=settings.PREFIX, comment_id=comment_id)
|
||||
|
||||
@@ -29,7 +29,6 @@ def search_trending_tags(course_id, query_params={}, *args, **kwargs):
|
||||
def tags_autocomplete(value, *args, **kwargs):
|
||||
return perform_request('get', _url_for_threads_tags_autocomplete(), {'value': value}, *args, **kwargs)
|
||||
|
||||
|
||||
def _url_for_search_similar_threads():
|
||||
return "{prefix}/search/threads/more_like_this".format(prefix=settings.PREFIX)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from .utils import *
|
||||
|
||||
import models
|
||||
import settings
|
||||
|
||||
@@ -11,7 +10,7 @@ class Thread(models.Model):
|
||||
'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id',
|
||||
'created_at', 'updated_at', 'comments_count', 'unread_comments_count',
|
||||
'at_position_list', 'children', 'type', 'highlighted_title',
|
||||
'highlighted_body', 'endorsed', 'read', 'group_id', 'group_name', 'pinned'
|
||||
'highlighted_body', 'endorsed', 'read', 'group_id', 'group_name', 'pinned', 'abuse_flaggers'
|
||||
]
|
||||
|
||||
updatable_fields = [
|
||||
@@ -27,11 +26,13 @@ class Thread(models.Model):
|
||||
|
||||
@classmethod
|
||||
def search(cls, query_params, *args, **kwargs):
|
||||
|
||||
default_params = {'page': 1,
|
||||
'per_page': 20,
|
||||
'course_id': query_params['course_id'],
|
||||
'recursive': False}
|
||||
params = merge_dict(default_params, strip_blank(strip_none(query_params)))
|
||||
|
||||
if query_params.get('text') or query_params.get('tags') or query_params.get('commentable_ids'):
|
||||
url = cls.url(action='search')
|
||||
else:
|
||||
@@ -54,6 +55,7 @@ class Thread(models.Model):
|
||||
|
||||
@classmethod
|
||||
def url(cls, action, params={}):
|
||||
|
||||
if action in ['get_all', 'post']:
|
||||
return cls.url_for_threads(params)
|
||||
elif action == 'search':
|
||||
@@ -66,12 +68,11 @@ class Thread(models.Model):
|
||||
# that subclasses don't need to override for this.
|
||||
def _retrieve(self, *args, **kwargs):
|
||||
url = self.url(action='get', params=self.attributes)
|
||||
|
||||
request_params = {
|
||||
'recursive': kwargs.get('recursive'),
|
||||
'user_id': kwargs.get('user_id'),
|
||||
'mark_as_read': kwargs.get('mark_as_read', True),
|
||||
}
|
||||
'recursive': kwargs.get('recursive'),
|
||||
'user_id': kwargs.get('user_id'),
|
||||
'mark_as_read': kwargs.get('mark_as_read', True),
|
||||
}
|
||||
|
||||
# user_id may be none, in which case it shouldn't be part of the
|
||||
# request.
|
||||
@@ -79,23 +80,57 @@ class Thread(models.Model):
|
||||
|
||||
response = perform_request('get', url, request_params)
|
||||
self.update_attributes(**response)
|
||||
|
||||
|
||||
def flagAbuse(self, user, voteable):
|
||||
if voteable.type == 'thread':
|
||||
url = _url_for_flag_abuse_thread(voteable.id)
|
||||
elif voteable.type == 'comment':
|
||||
url = _url_for_flag_comment(voteable.id)
|
||||
else:
|
||||
raise CommentClientError("Can only flag/unflag threads or comments")
|
||||
params = {'user_id': user.id}
|
||||
request = perform_request('put', url, params)
|
||||
voteable.update_attributes(request)
|
||||
|
||||
def unFlagAbuse(self, user, voteable, removeAll):
|
||||
if voteable.type == 'thread':
|
||||
url = _url_for_unflag_abuse_thread(voteable.id)
|
||||
elif voteable.type == 'comment':
|
||||
url = _url_for_unflag_comment(voteable.id)
|
||||
else:
|
||||
raise CommentClientError("Can only flag/unflag for threads or comments")
|
||||
params = {'user_id': user.id}
|
||||
#if you're an admin, when you unflag, remove ALL flags
|
||||
if removeAll:
|
||||
params['all'] = True
|
||||
|
||||
request = perform_request('put', url, params)
|
||||
voteable.update_attributes(request)
|
||||
|
||||
def pin(self, user, thread_id):
|
||||
url = _url_for_pin_thread(thread_id)
|
||||
params = {'user_id': user.id}
|
||||
request = perform_request('put', url, params)
|
||||
self.update_attributes(request)
|
||||
self.update_attributes(request)
|
||||
|
||||
def un_pin(self, user, thread_id):
|
||||
url = _url_for_un_pin_thread(thread_id)
|
||||
params = {'user_id': user.id}
|
||||
request = perform_request('put', url, params)
|
||||
self.update_attributes(request)
|
||||
|
||||
|
||||
self.update_attributes(request)
|
||||
|
||||
|
||||
def _url_for_flag_abuse_thread(thread_id):
|
||||
return "{prefix}/threads/{thread_id}/abuse_flag".format(prefix=settings.PREFIX, thread_id=thread_id)
|
||||
|
||||
|
||||
def _url_for_unflag_abuse_thread(thread_id):
|
||||
return "{prefix}/threads/{thread_id}/abuse_unflag".format(prefix=settings.PREFIX, thread_id=thread_id)
|
||||
|
||||
|
||||
def _url_for_pin_thread(thread_id):
|
||||
return "{prefix}/threads/{thread_id}/pin".format(prefix=settings.PREFIX, thread_id=thread_id)
|
||||
|
||||
return "{prefix}/threads/{thread_id}/pin".format(prefix=settings.PREFIX, thread_id=thread_id)
|
||||
|
||||
|
||||
def _url_for_un_pin_thread(thread_id):
|
||||
return "{prefix}/threads/{thread_id}/unpin".format(prefix=settings.PREFIX, thread_id=thread_id)
|
||||
|
||||
return "{prefix}/threads/{thread_id}/unpin".format(prefix=settings.PREFIX, thread_id=thread_id)
|
||||
|
||||
BIN
lms/static/images/flagged.png
Normal file
BIN
lms/static/images/flagged.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
lms/static/images/notflagged.png
Normal file
BIN
lms/static/images/notflagged.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
lms/static/images/resolvedflag.png
Normal file
BIN
lms/static/images/resolvedflag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 362 B |
@@ -95,6 +95,7 @@
|
||||
|
||||
|
||||
body.discussion {
|
||||
|
||||
.new-post-form-errors {
|
||||
display: none;
|
||||
background: $error-red;
|
||||
@@ -1280,8 +1281,8 @@ body.discussion {
|
||||
.discussion-article {
|
||||
position: relative;
|
||||
padding: 40px;
|
||||
min-height: 468px;
|
||||
|
||||
min-height: 468px;
|
||||
|
||||
a {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@@ -1334,6 +1335,9 @@ body.discussion {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
.discussion-post {
|
||||
@@ -2436,7 +2440,6 @@ body.discussion {
|
||||
@extend .discussion-module
|
||||
}
|
||||
|
||||
|
||||
.group-visibility-label {
|
||||
font-size: 12px;
|
||||
color:#000;
|
||||
@@ -2450,6 +2453,15 @@ body.discussion {
|
||||
padding-right: 5px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.discussion-pin-inline {
|
||||
font-size: 12px;
|
||||
float:right;
|
||||
font-style: italic;
|
||||
position: relative;
|
||||
right:-20px;
|
||||
top:-13px;
|
||||
}
|
||||
|
||||
.notpinned .icon
|
||||
{
|
||||
@@ -2477,4 +2489,44 @@ body.discussion {
|
||||
.notpinned span {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.pinned-false
|
||||
{
|
||||
display:none;
|
||||
}
|
||||
|
||||
.discussion-flag-abuse {
|
||||
font-size: 12px;
|
||||
float:right;
|
||||
padding-right: 5px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.notflagged .icon
|
||||
{
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 14px;
|
||||
padding-right: 3px;
|
||||
background: transparent url('../images/notflagged.png') no-repeat 0 0;
|
||||
}
|
||||
|
||||
.flagged .icon
|
||||
{
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 14px;
|
||||
padding-right: 3px;
|
||||
background: transparent url('../images/flagged.png') no-repeat 0 0;
|
||||
}
|
||||
|
||||
.flagged span {
|
||||
color: #B82066;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.notflagged span {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -27,6 +27,42 @@ def url_class(is_active):
|
||||
</li>
|
||||
% endfor
|
||||
<%block name="extratabs" />
|
||||
% if masquerade is not UNDEFINED:
|
||||
% if staff_access and masquerade is not None:
|
||||
<li style="float:right"><a href="#" id="staffstatus">Staff view</a></li>
|
||||
% endif
|
||||
% endif
|
||||
</ol>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
% if masquerade is not UNDEFINED:
|
||||
% if staff_access and masquerade is not None:
|
||||
<script type="text/javascript">
|
||||
masq = (function(){
|
||||
var el = $('#staffstatus');
|
||||
var setstat = function(status){
|
||||
if (status=='student'){
|
||||
el.html('<font color="green">Student view</font>');
|
||||
}else{
|
||||
el.html('<font color="red">Staff view</font>');
|
||||
}
|
||||
}
|
||||
setstat('${masquerade}');
|
||||
|
||||
el.click(function(){
|
||||
$.ajax({ url: '/masquerade/toggle',
|
||||
type: 'GET',
|
||||
success: function(result){
|
||||
setstat(result.status);
|
||||
location.reload();
|
||||
},
|
||||
error: function() {
|
||||
alert('Error: cannot connect to server');
|
||||
}
|
||||
});
|
||||
});
|
||||
}() );
|
||||
</script>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
@@ -33,6 +33,14 @@
|
||||
<span class="board-name" data-discussion_id='#all'>Show All Discussions</span>
|
||||
</a>
|
||||
</li>
|
||||
%if flag_moderator:
|
||||
<li>
|
||||
<a href="#">
|
||||
<span class="board-name" data-discussion_id='#flagged'>Show Flagged Discussions</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
%endif
|
||||
<li>
|
||||
<a href="#">
|
||||
<span class="board-name" data-discussion_id='#following'>Following</span>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<script type="text/template" id="thread-template">
|
||||
<article class="discussion-article" data-id="${'<%- id %>'}">
|
||||
<div class="thread-content-wrapper"></div>
|
||||
|
||||
<ol class="responses">
|
||||
<li class="loading"><div class="loading-animation"></div></li>
|
||||
</ol>
|
||||
@@ -30,7 +31,8 @@
|
||||
<div class="group-visibility-label">${"<%- obj.group_string%>"}</div>
|
||||
${"<% } %>"}
|
||||
|
||||
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote" data-tooltip="vote"><span class="plus-icon">+</span> <span class='votes-count-number'>${'<%- votes["up_count"] %>'}</span></a>
|
||||
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote" data-tooltip="vote">
|
||||
<span class="plus-icon">+</span> <span class='votes-count-number'>${'<%- votes["up_count"] %>'}</span></a>
|
||||
<h1>${'<%- title %>'}</h1>
|
||||
<p class="posted-details">
|
||||
${"<% if (obj.username) { %>"}
|
||||
@@ -45,6 +47,10 @@
|
||||
</header>
|
||||
|
||||
<div class="post-body">${'<%- body %>'}</div>
|
||||
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="report misuse">
|
||||
<i class="icon"></i><span class="flag-label">Report Misuse</span></div>
|
||||
|
||||
|
||||
% if course and has_permission(user, 'openclose_thread', course.id):
|
||||
<div class="admin-pin discussion-pin notpinned" data-role="thread-pin" data-tooltip="pin this thread">
|
||||
<i class="icon"></i><span class="pin-label">Pin Thread</span></div>
|
||||
@@ -118,7 +124,10 @@
|
||||
${"<% } else {print('<span class=\"anonymous\"><em>anonymous</em></span>');} %>"}
|
||||
<p class="posted-details" title="${'<%- created_at %>'}">${'<%- created_at %>'}</p>
|
||||
</header>
|
||||
<div class="response-local"><div class="response-body">${"<%- body %>"}</div></div>
|
||||
<div class="response-local"><div class="response-body">${"<%- body %>"}</div>
|
||||
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="report misuse">
|
||||
<i class="icon"></i><span class="flag-label">Report Misuse</span></div>
|
||||
</div>
|
||||
<ul class="moderator-actions response-local">
|
||||
<li style="display: none"><a class="action-edit" href="javascript:void(0)"><span class="edit-icon"></span> Edit</a></li>
|
||||
<li style="display: none"><a class="action-delete" href="javascript:void(0)"><span class="delete-icon"></span> Delete</a></li>
|
||||
@@ -141,6 +150,8 @@
|
||||
<script type="text/template" id="response-comment-show-template">
|
||||
<div id="comment_${'<%- id %>'}">
|
||||
<div class="response-body">${'<%- body %>'}</div>
|
||||
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="report misuse">
|
||||
<i class="icon"></i><span class="flag-label"></span></div>
|
||||
<p class="posted-details">–posted <span class="timeago" title="${'<%- created_at %>'}">${'<%- created_at %>'}</span> by
|
||||
${"<% if (obj.username) { %>"}
|
||||
<a href="${'<%- user_url %>'}" class="profile-link">${'<%- username %>'}</a>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<%include file="_new_post.html" />
|
||||
|
||||
<section class="discussion container" id="discussion-container" data-roles="${roles}" data-course-id="${course_id}" data-user-info="${user_info}" data-threads="${threads}" data-thread-pages="${thread_pages}" data-content-info="${annotated_content_info}">
|
||||
<section class="discussion container" id="discussion-container" data-roles="${roles}" data-course-id="${course_id}" data-user-info="${user_info}" data-threads="${threads}" data-thread-pages="${thread_pages}" data-content-info="${annotated_content_info}" data-flag-moderator="${flag_moderator}">
|
||||
<div class="discussion-body">
|
||||
<div class="sidebar"></div>
|
||||
<div class="discussion-column">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<article class="discussion-article" data-id="{{id}}">
|
||||
<div class="group-visibility-label">{{group_string}}</div>
|
||||
|
||||
<div class="thread-content-wrapper"></div>
|
||||
|
||||
<ol class="responses post-extended-content">
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
<header>
|
||||
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote" data-tooltip="vote"><span class="plus-icon">+</span> <span class='votes-count-number'>{{votes.up_count}}</span></a>
|
||||
<h3>{{title}}</h3>
|
||||
<div class="discussion-pin-inline pinned pinned-{{pinned}}" data-tooltip="This thread has been pinned by course staff.">
|
||||
<i class="icon"></i><span class="pin-label">Pinned</span></div>
|
||||
|
||||
<p class="posted-details">
|
||||
{{#user}}
|
||||
<a href="{{user_url}}" class="username">{{username}}</a>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<%include file="_new_post.html" />
|
||||
|
||||
<section class="discussion container" id="discussion-container" data-roles="${roles}" data-course-id="${course_id}" data-user-info="${user_info}" data-threads="${threads}" data-content-info="${annotated_content_info}" data-thread-pages="${thread_pages}">
|
||||
<section class="discussion container" id="discussion-container" data-roles="${roles}" data-course-id="${course_id}" data-user-info="${user_info}" data-threads="${threads}" data-content-info="${annotated_content_info}" data-thread-pages="${thread_pages}" data-flag-moderator="${flag_moderator}">
|
||||
<div class="discussion-body">
|
||||
<div class="sidebar"></div>
|
||||
<div class="discussion-column"></div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<id>tag:www.edx.org,2012:Post/17</id>
|
||||
<published>2012-12-19T14:00:00-07:00</published>
|
||||
<updated>2012-12-19T14:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/stanford-to-work-with-edx')}"/>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['stanford-to-work-with-edx'])}"/>
|
||||
<title>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/stanford-university-m.png')}" />
|
||||
<p></p></content>
|
||||
@@ -20,7 +20,7 @@
|
||||
<id>tag:www.edx.org,2013:Post/16</id>
|
||||
<published>2013-03-15T10:00:00-07:00</published>
|
||||
<updated>2013-03-15T10:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/xblock-announcement')}"/>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['xblock-announcement'])}"/>
|
||||
<title>edX releases XBlock SDK, first step toward open source vision</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/edx-logo_240x180.png')}" />
|
||||
<p></p></content>
|
||||
@@ -38,7 +38,7 @@
|
||||
<!-- <id>tag:www.edx.org,2013:Post/14</id> -->
|
||||
<!-- <published>2013-02-20T10:00:00-07:00</published> -->
|
||||
<!-- <updated>2013-02-20T10:00:00-07:00</updated> -->
|
||||
<!-- <link type="text/html" rel="alternate" href="${reverse('press/edx-expands-internationally')}"/> -->
|
||||
<!-- <link type="text/html" rel="alternate" href="${reverse('press_release', args=['edx-expands-internationally'])}"/> -->
|
||||
<!-- <title>edX Expands Internationally and Doubles its Institutional Membership with the Addition of Six New Schools</title> -->
|
||||
<!-- <content type="html"><img src="${static.url('images/press/releases/edx-logo_240x180.png')}" /> -->
|
||||
<!-- <p></p></content> -->
|
||||
@@ -47,7 +47,7 @@
|
||||
<id>tag:www.edx.org,2013:Post/14</id>
|
||||
<published>2013-01-30T10:00:00-07:00</published>
|
||||
<updated>2013-01-30T10:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/eric-lander-secret-of-life')}"/>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['eric-lander-secret-of-life'])}"/>
|
||||
<title>New biology course from human genome pioneer Eric Lander</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/eric-lander_240x180.jpg')}" />
|
||||
<p></p></content>
|
||||
@@ -56,7 +56,7 @@
|
||||
<id>tag:www.edx.org,2013:Post/12</id>
|
||||
<published>2013-01-22T10:00:00-07:00</published>
|
||||
<updated>2013-01-22T10:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/lewin-course-announcement')}"/>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['lewin-course-announcement'])}"/>
|
||||
<title>New course from legendary MIT physics professor Walter Lewin</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/dr-lewin-316_240x180.jpg')}" />
|
||||
<p></p></content>
|
||||
@@ -65,7 +65,7 @@
|
||||
<id>tag:www.edx.org,2013:Post/11</id>
|
||||
<published>2013-01-29T10:00:00-07:00</published>
|
||||
<updated>2013-01-29T10:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/bostonx-announcement')}"/>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['bostonx-announcement'])}"/>
|
||||
<title>City of Boston and edX partner to establish BostonX to improve educational access for residents</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/edx-logo_240x180.png')}" />
|
||||
<p></p></content>
|
||||
@@ -74,7 +74,7 @@
|
||||
<!-- <id>tag:www.edx.org,2012:Post/10</id> -->
|
||||
<!-- <published>2012-12-19T14:00:00-07:00</published> -->
|
||||
<!-- <updated>2012-12-19T14:00:00-07:00</updated> -->
|
||||
<!-- <link type="text/html" rel="alternate" href="${reverse('press/spring-courses')}"/> -->
|
||||
<!-- <link type="text/html" rel="alternate" href="${reverse('press_release', args=['spring-courses'])}"/> -->
|
||||
<!-- <title>edX announces first wave of new courses for Spring 2013</title> -->
|
||||
<!-- <content type="html"><img src="${static.url('images/press/releases/edx-logo_240x180.png')}" /> -->
|
||||
<!-- <p></p></content> -->
|
||||
@@ -83,7 +83,7 @@
|
||||
<id>tag:www.edx.org,2012:Post/9</id>
|
||||
<published>2012-12-10T14:00:00-07:00</published>
|
||||
<updated>2012-12-10T14:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/georgetown-joins-edx')}"/>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['georgetown-joins-edx'])}"/>
|
||||
<title>Georgetown University joins edX</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/georgetown-seal_240x180.png')}" />
|
||||
<p>Sixth institution to join global movement in year one</p></content>
|
||||
@@ -92,7 +92,7 @@
|
||||
<id>tag:www.edx.org,2012:Post/8</id>
|
||||
<published>2012-12-04T14:00:00-07:00</published>
|
||||
<updated>2012-12-04T14:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/wellesley-college-joins-edx')}"/>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['wellesley-college-joins-edx'])}"/>
|
||||
<title>Wellesley College joins edX</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/wellesley-seal_240x180.png')}" />
|
||||
<p>First liberal arts college to join edX</p></content>
|
||||
@@ -101,7 +101,7 @@
|
||||
<id>tag:www.edx.org,2012:Post/7</id>
|
||||
<published>2012-11-12T14:00:00-07:00</published>
|
||||
<updated>2012-11-12T14:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/gates-foundation-announcement')}"/>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['gates-foundation-announcement'])}"/>
|
||||
<title>edX and Massachusetts Community Colleges join in Gates-Funded educational initiative</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/mass-seal_240x180.png')}" />
|
||||
<p></p></content>
|
||||
@@ -110,7 +110,7 @@
|
||||
<id>tag:www.edx.org,2012:Post/6</id>
|
||||
<published>2012-10-15T14:00:00-07:00</published>
|
||||
<updated>2012-10-14T14:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/ut-joins-edx')}"/>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['ut-joins-edx'])}"/>
|
||||
<title>The University of Texas System joins edX</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/utsys-seal_240x180.png')}" />
|
||||
<p>Nine universities and six health institutions</p></content>
|
||||
@@ -119,7 +119,7 @@
|
||||
<!-- <id>tag:www.edx.org,2012:Post/5</id> -->
|
||||
<!-- <published>2012-09-25T14:00:00-07:00</published> -->
|
||||
<!-- <updated>2012-09-25T14:00:00-07:00</updated> -->
|
||||
<!-- <link type="text/html" rel="alternate" href="${reverse('press/elsevier-collaborates-with-edx')}"/> -->
|
||||
<!-- <link type="text/html" rel="alternate" href="${reverse('press_release', args=['elsevier-collaborates-with-edx'])}"/> -->
|
||||
<!-- <title>Elsevier collaborates with edX</title> -->
|
||||
<!-- <content type="html"><img src="${static.url('images/press/releases/foundations-of-analog_240x180.jpg')}" /> -->
|
||||
<!-- <p>Free course textbook made available to edX students</p></content> -->
|
||||
@@ -128,7 +128,7 @@
|
||||
<id>tag:www.edx.org,2012:Post/4</id>
|
||||
<published>2012-09-06T14:00:00-07:00</published>
|
||||
<updated>2012-09-06T14:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/edX-announces-proctored-exam-testing')}"/>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['edX-announces-proctored-exam-testing'])}"/>
|
||||
<title>edX to offer learners option of taking proctored final exam</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/diploma_240x180.jpg')}" /></content>
|
||||
</entry>
|
||||
@@ -136,7 +136,7 @@
|
||||
<id>tag:www.edx.org,2012:Post/3</id>
|
||||
<published>2012-07-16T14:08:12-07:00</published>
|
||||
<updated>2012-07-16T14:08:12-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/uc-berkeley-joins-edx')}"/>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press_release', args=['uc-berkeley-joins-edx'])}"/>
|
||||
<title>UC Berkeley joins edX</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/edx-logo_240x180.png')}" />
|
||||
<p>edX broadens course offerings</p></content>
|
||||
|
||||
56
lms/urls.py
56
lms/urls.py
@@ -117,51 +117,9 @@ urlpatterns = ('',
|
||||
{'template': 'honor.html'}, name="honor"),
|
||||
|
||||
#Press releases
|
||||
url(r'^press/mit-and-harvard-announce-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/MIT_and_Harvard_announce_edX.html'}, name="press/mit-and-harvard-announce-edx"),
|
||||
url(r'^press/uc-berkeley-joins-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/UC_Berkeley_joins_edX.html'}, name="press/uc-berkeley-joins-edx"),
|
||||
url(r'^press/edX-announces-proctored-exam-testing$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/edX_announces_proctored_exam_testing.html'}, name="press/edX-announces-proctored-exam-testing"),
|
||||
url(r'^press/elsevier-collaborates-with-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Elsevier_collaborates_with_edX.html'}, name="press/elsevier-collaborates-with-edx"),
|
||||
url(r'^press/ut-joins-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/UT_joins_edX.html'}, name="press/ut-joins-edx"),
|
||||
url(r'^press/cengage-to-provide-book-content$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Cengage_to_provide_book_content.html'}, name="press/cengage-to-provide-book-content"),
|
||||
url(r'^press/gates-foundation-announcement$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Gates_Foundation_announcement.html'}, name="press/gates-foundation-announcement"),
|
||||
url(r'^press/wellesley-college-joins-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Wellesley_College_joins_edX.html'}, name="press/wellesley-college-joins-edx"),
|
||||
url(r'^press/georgetown-joins-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Georgetown_joins_edX.html'}, name="press/georgetown-joins-edx"),
|
||||
url(r'^press/spring-courses$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Spring_2013_course_announcements.html'},
|
||||
name="press/spring-courses"),
|
||||
url(r'^press/lewin-course-announcement$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Lewin_course_announcement.html'},
|
||||
name="press/lewin-course-announcement"),
|
||||
url(r'^press/bostonx-announcement$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/bostonx_announcement.html'},
|
||||
name="press/bostonx-announcement"),
|
||||
url(r'^press/eric-lander-secret-of-life$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/eric_lander_secret_of_life.html'},
|
||||
name="press/eric-lander-secret-of-life"),
|
||||
url(r'^press/edx-expands-internationally$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/edx_expands_internationally.html'},
|
||||
name="press/edx-expands-internationally"),
|
||||
url(r'^press/xblock_announcement$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/xblock_announcement.html'},
|
||||
name="press/xblock-announcement"),
|
||||
url(r'^press/stanford-to-work-with-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/stanford_announcement.html'},
|
||||
name="press/stanford-to-work-with-edx"),
|
||||
|
||||
# Should this always update to point to the latest press release?
|
||||
(r'^pressrelease$', 'django.views.generic.simple.redirect_to',
|
||||
{'url': '/press/xblock-announcement'}),
|
||||
|
||||
url(r'^press/([_a-zA-Z0-9-]+)$', 'static_template_view.views.render_press_release', name='press_release'),
|
||||
|
||||
# Favicon
|
||||
(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),
|
||||
|
||||
# TODO: These urls no longer work. They need to be updated before they are re-enabled
|
||||
@@ -201,9 +159,6 @@ if settings.WIKI_ENABLED:
|
||||
|
||||
if settings.COURSEWARE_ENABLED:
|
||||
urlpatterns += (
|
||||
# Hook django-masquerade, allowing staff to view site as other users
|
||||
url(r'^masquerade/', include('masquerade.urls')),
|
||||
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/jump_to/(?P<location>.*)$',
|
||||
'courseware.views.jump_to', name="jump_to"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
|
||||
@@ -341,6 +296,13 @@ if settings.COURSEWARE_ENABLED:
|
||||
'open_ended_grading.views.peer_grading', name='peer_grading'),
|
||||
)
|
||||
|
||||
# allow course staff to change to student view of courseware
|
||||
if settings.MITX_FEATURES.get('ENABLE_MASQUERADE'):
|
||||
urlpatterns += (
|
||||
url(r'^masquerade/(?P<marg>.*)$','courseware.masquerade.handle_ajax', name="masquerade-switch"),
|
||||
)
|
||||
|
||||
|
||||
# discussion forums live within courseware, so courseware must be enabled first
|
||||
if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
|
||||
urlpatterns += (
|
||||
|
||||
@@ -61,8 +61,8 @@ sphinx==1.1.3
|
||||
|
||||
# Used for testing
|
||||
coverage==3.6
|
||||
factory_boy==1.3.0
|
||||
lettuce==0.2.15
|
||||
factory_boy==2.0.2
|
||||
lettuce==0.2.16
|
||||
mock==0.8.0
|
||||
nosexcover==1.0.7
|
||||
pep8==1.4.5
|
||||
|
||||
Reference in New Issue
Block a user