Commit f69d38bb by cahrens

Merge branch 'master' into feature/christina/metadata

parents c0f0366b 57427f9a
......@@ -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()
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
......
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
\ No newline at end of file
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">&#x2709;</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&quot;${context_course.display_name_with_default}&quot;,%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">&#x2709;</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'
......
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);
}
}
......
......@@ -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)
......
......@@ -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]
......
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)
\ No newline at end of file
......@@ -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()
......
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&nbsp;&nbsp;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
......
......@@ -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".
......
......@@ -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'],
......
'''
---------------------------------------- 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
......
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
class UserProfileFactory(factory.Factory):
FACTORY_FOR = UserProfile
from xmodule.modulestore import Location
user = None
name = 'Robot Studio'
courseware = 'course.xml'
location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
class RegistrationFactory(factory.Factory):
FACTORY_FOR = Registration
class UserProfileFactory(StudentUserProfileFactory):
name = 'Robot Studio'
courseware = 'course.xml'
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
class CourseEnrollmentAllowedFactory(StudentCourseEnrollmentAllowedFactory):
pass
email = 'test@edx.org'
course_id = 'edX/test/2012_Fall'
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)
"""
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)
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)
......@@ -287,25 +287,71 @@ def vote_for_thread(request, course_id, thread_id, value):
@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
def undo_vote_for_thread(request, course_id, thread_id):
user = cc.User.from_django_user(request.user)
thread = cc.Thread.find(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)
\ No newline at end of file
......@@ -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')
#########################################################################################
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'
......
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',
......
"""
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):
"""
Render a normal page, like jobs
"""
response = self.client.get("/jobs")
self.assertEquals(response.status_code, 200)
def test_render_press_release(self):
"""
Tests that 1 + 1 always equals 2.
Render press releases from generic URL match
"""
self.assertEqual(1 + 1, 2)
# 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)
......
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)
\ No newline at end of file
return "{prefix}/threads/{thread_id}/unpin".format(prefix=settings.PREFIX, thread_id=thread_id)
......@@ -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;
}
\ No newline at end of file
......@@ -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">&ndash;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">
......
<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">&lt;img src=&quot;${static.url('images/press/releases/stanford-university-m.png')}&quot; /&gt;
&lt;p&gt;&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/edx-logo_240x180.png')}&quot; /&gt;
&lt;p&gt;&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/edx-logo_240x180.png')}&quot; /&gt; -->
<!-- &lt;p&gt;&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/eric-lander_240x180.jpg')}&quot; /&gt;
&lt;p&gt;&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/dr-lewin-316_240x180.jpg')}&quot; /&gt;
&lt;p&gt;&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/edx-logo_240x180.png')}&quot; /&gt;
&lt;p&gt;&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/edx-logo_240x180.png')}&quot; /&gt; -->
<!-- &lt;p&gt;&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/georgetown-seal_240x180.png')}&quot; /&gt;
&lt;p&gt;Sixth institution to join global movement in year one&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/wellesley-seal_240x180.png')}&quot; /&gt;
&lt;p&gt;First liberal arts college to join edX&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/mass-seal_240x180.png')}&quot; /&gt;
&lt;p&gt;&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/utsys-seal_240x180.png')}&quot; /&gt;
&lt;p&gt;Nine universities and six health institutions&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/foundations-of-analog_240x180.jpg')}&quot; /&gt; -->
<!-- &lt;p&gt;Free course textbook made available to edX students&lt;/p&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/diploma_240x180.jpg')}&quot; /&gt;</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">&lt;img src=&quot;${static.url('images/press/releases/edx-logo_240x180.png')}&quot; /&gt;
&lt;p&gt;edX broadens course offerings&lt;/p&gt;</content>
......
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment