diff --git a/.gitignore b/.gitignore
index 2fd1ca0181..493df5a7fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,4 +27,5 @@ lms/lib/comment_client/python
nosetests.xml
cover_html/
.idea/
+.redcar/
chromedriver.log
\ No newline at end of file
diff --git a/.redcar/lucene/segments.gen b/.redcar/lucene/segments.gen
new file mode 100644
index 0000000000..568652b956
Binary files /dev/null and b/.redcar/lucene/segments.gen differ
diff --git a/.redcar/lucene_last_updated b/.redcar/lucene_last_updated
new file mode 100644
index 0000000000..3692c8e076
--- /dev/null
+++ b/.redcar/lucene_last_updated
@@ -0,0 +1 @@
+1360614836
diff --git a/.redcar/redcar.lock b/.redcar/redcar.lock
new file mode 100644
index 0000000000..0677ede437
--- /dev/null
+++ b/.redcar/redcar.lock
@@ -0,0 +1 @@
+10664: Locked by 10664 at Mon Feb 11 14:22:22 -0500 2013
diff --git a/.redcar/storage/cursor_saver.yaml b/.redcar/storage/cursor_saver.yaml
new file mode 100644
index 0000000000..ba9b833044
--- /dev/null
+++ b/.redcar/storage/cursor_saver.yaml
@@ -0,0 +1,4 @@
+---
+cursor_positions: []
+
+files_to_retain: 0
diff --git a/.redcar/tags.REMOVED.git-id b/.redcar/tags.REMOVED.git-id
new file mode 100644
index 0000000000..f8318e5f1f
--- /dev/null
+++ b/.redcar/tags.REMOVED.git-id
@@ -0,0 +1 @@
+ce76efcea5f0a5b2238364f81d54f1d393853a1a
\ No newline at end of file
diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js
index 168cb960be..97d71f6c79 100644
--- a/cms/static/js/models/settings/course_details.js
+++ b/cms/static/js/models/settings/course_details.js
@@ -68,10 +68,10 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
save_videosource: function(newsource) {
// newsource either is or just the "speed:key, *" string
// returns the videosource for the preview which iss the key whose speed is closest to 1
- if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.set({'intro_video': null});
+ if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
// TODO remove all whitespace w/in string
else {
- if (this.get('intro_video') !== newsource) this.set('intro_video', newsource);
+ if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
}
return this.videosourceSample();
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
index 155f82e0c7..f0234ec71a 100644
--- a/common/djangoapps/course_groups/cohorts.py
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -6,6 +6,7 @@ forums, and to the cohort admin views.
from django.contrib.auth.models import User
from django.http import Http404
import logging
+import random
from courseware import courses
from student.models import get_user_by_username_or_email
@@ -64,7 +65,23 @@ def is_commentable_cohorted(course_id, commentable_id):
ans))
return ans
+
+def get_cohorted_commentables(course_id):
+ """
+ Given a course_id return a list of strings representing cohorted commentables
+ """
+ course = courses.get_course_by_id(course_id)
+
+ if not course.is_cohorted:
+ # this is the easy case :)
+ ans = []
+ else:
+ ans = course.cohorted_discussions
+
+ return ans
+
+
def get_cohort(user, course_id):
"""
Given a django User and a course_id, return the user's cohort in that
@@ -96,9 +113,30 @@ def get_cohort(user, course_id):
group_type=CourseUserGroup.COHORT,
users__id=user.id)
except CourseUserGroup.DoesNotExist:
- # TODO: add auto-cohorting logic here once we know what that will be.
+ # Didn't find the group. We'll go on to create one if needed.
+ pass
+
+ if not course.auto_cohort:
return None
+ choices = course.auto_cohort_groups
+ if len(choices) == 0:
+ # Nowhere to put user
+ log.warning("Course %s is auto-cohorted, but there are no"
+ " auto_cohort_groups specified",
+ course_id)
+ return None
+
+ # Put user in a random group, creating it if needed
+ group_name = random.choice(choices)
+ group, created = CourseUserGroup.objects.get_or_create(
+ course_id=course_id,
+ group_type=CourseUserGroup.COHORT,
+ name=group_name)
+
+ user.course_groups.add(group)
+ return group
+
def get_course_cohorts(course_id):
"""
diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py
index b3ad928b39..efed39d536 100644
--- a/common/djangoapps/course_groups/tests/tests.py
+++ b/common/djangoapps/course_groups/tests/tests.py
@@ -47,7 +47,10 @@ class TestCohorts(django.test.TestCase):
@staticmethod
def config_course_cohorts(course, discussions,
- cohorted, cohorted_discussions=None):
+ cohorted,
+ cohorted_discussions=None,
+ auto_cohort=None,
+ auto_cohort_groups=None):
"""
Given a course with no discussion set up, add the discussions and set
the cohort config appropriately.
@@ -59,6 +62,9 @@ class TestCohorts(django.test.TestCase):
cohorted: bool.
cohorted_discussions: optional list of topic names. If specified,
converts them to use the same ids as topic names.
+ auto_cohort: optional bool.
+ auto_cohort_groups: optional list of strings
+ (names of groups to put students into).
Returns:
Nothing -- modifies course in place.
@@ -76,6 +82,12 @@ class TestCohorts(django.test.TestCase):
if cohorted_discussions is not None:
d["cohorted_discussions"] = [to_id(name)
for name in cohorted_discussions]
+
+ if auto_cohort is not None:
+ d["auto_cohort"] = auto_cohort
+ if auto_cohort_groups is not None:
+ d["auto_cohort_groups"] = auto_cohort_groups
+
course.metadata["cohort_config"] = d
@@ -89,12 +101,9 @@ class TestCohorts(django.test.TestCase):
def test_get_cohort(self):
- # Need to fix this, but after we're testing on staging. (Looks like
- # problem is that when get_cohort internally tries to look up the
- # course.id, it fails, even though we loaded it through the modulestore.
-
- # Proper fix: give all tests a standard modulestore that uses the test
- # dir.
+ """
+ Make sure get_cohort() does the right thing when the course is cohorted
+ """
course = modulestore().get_course("edX/toy/2012_Fall")
self.assertEqual(course.id, "edX/toy/2012_Fall")
self.assertFalse(course.is_cohorted)
@@ -122,6 +131,54 @@ class TestCohorts(django.test.TestCase):
self.assertEquals(get_cohort(other_user, course.id), None,
"other_user shouldn't have a cohort")
+ def test_auto_cohorting(self):
+ """
+ Make sure get_cohort() does the right thing when the course is auto_cohorted
+ """
+ course = modulestore().get_course("edX/toy/2012_Fall")
+ self.assertEqual(course.id, "edX/toy/2012_Fall")
+ self.assertFalse(course.is_cohorted)
+
+ user1 = User.objects.create(username="test", email="a@b.com")
+ user2 = User.objects.create(username="test2", email="a2@b.com")
+ user3 = User.objects.create(username="test3", email="a3@b.com")
+
+ cohort = CourseUserGroup.objects.create(name="TestCohort",
+ course_id=course.id,
+ group_type=CourseUserGroup.COHORT)
+
+ # user1 manually added to a cohort
+ cohort.users.add(user1)
+
+ # Make the course auto cohorted...
+ self.config_course_cohorts(course, [], cohorted=True,
+ auto_cohort=True,
+ auto_cohort_groups=["AutoGroup"])
+
+ self.assertEquals(get_cohort(user1, course.id).id, cohort.id,
+ "user1 should stay put")
+
+ self.assertEquals(get_cohort(user2, course.id).name, "AutoGroup",
+ "user2 should be auto-cohorted")
+
+ # Now make the group list empty
+ self.config_course_cohorts(course, [], cohorted=True,
+ auto_cohort=True,
+ auto_cohort_groups=[])
+
+ self.assertEquals(get_cohort(user3, course.id), None,
+ "No groups->no auto-cohorting")
+
+ # Now make it different
+ self.config_course_cohorts(course, [], cohorted=True,
+ auto_cohort=True,
+ auto_cohort_groups=["OtherGroup"])
+
+ self.assertEquals(get_cohort(user3, course.id).name, "OtherGroup",
+ "New list->new group")
+ self.assertEquals(get_cohort(user2, course.id).name, "AutoGroup",
+ "user2 should still be in originally placed cohort")
+
def test_get_course_cohorts(self):
course1_id = 'a/b/c'
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 4635cc6871..a115a54376 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -429,6 +429,11 @@ class CapaModule(XModule):
# used by conditional module
return self.attempts > 0
+ def is_correct(self):
+ """True if full points"""
+ d = self.get_score()
+ return d['score'] == d['total']
+
def answer_available(self):
'''
Is the user allowed to see an answer?
@@ -449,6 +454,9 @@ class CapaModule(XModule):
return self.lcp.done
elif self.show_answer == 'closed':
return self.closed()
+ elif self.show_answer == 'finished':
+ return self.closed() or self.is_correct()
+
elif self.show_answer == 'past_due':
return self.is_past_due()
elif self.show_answer == 'always':
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 8b2d5a6c92..2ed780fcae 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -378,6 +378,28 @@ class CourseDescriptor(SequenceDescriptor):
return bool(config.get("cohorted"))
+ @property
+ def auto_cohort(self):
+ """
+ Return whether the course is auto-cohorted.
+ """
+ if not self.is_cohorted:
+ return False
+
+ return bool(self.metadata.get("cohort_config", {}).get(
+ "auto_cohort", False))
+
+ @property
+ def auto_cohort_groups(self):
+ """
+ Return the list of groups to put students into. Returns [] if not
+ specified. Returns specified list even if is_cohorted and/or auto_cohort are
+ false.
+ """
+ return self.metadata.get("cohort_config", {}).get(
+ "auto_cohort_groups", [])
+
+
@property
def top_level_discussion_topic_ids(self):
"""
@@ -714,7 +736,7 @@ class CourseDescriptor(SequenceDescriptor):
def get_test_center_exam(self, exam_series_code):
exams = [exam for exam in self.test_center_exams if exam.exam_series_code == exam_series_code]
return exams[0] if len(exams) == 1 else None
-
+
@property
def title(self):
return self.display_name
diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py
index a9df6c3504..0ba7e36540 100644
--- a/common/lib/xmodule/xmodule/modulestore/__init__.py
+++ b/common/lib/xmodule/xmodule/modulestore/__init__.py
@@ -23,6 +23,15 @@ URL_RE = re.compile("""
(@(?P[^/]+))?
""", re.VERBOSE)
+MISSING_SLASH_URL_RE = re.compile("""
+ (?P[^:]+):/
+ (?P[^/]+)/
+ (?P[^/]+)/
+ (?P[^/]+)/
+ (?P[^@]+)
+ (@(?P[^/]+))?
+ """, re.VERBOSE)
+
# TODO (cpennington): We should decide whether we want to expand the
# list of valid characters in a location
INVALID_CHARS = re.compile(r"[^\w.-]")
@@ -164,12 +173,16 @@ class Location(_LocationBase):
if isinstance(location, basestring):
match = URL_RE.match(location)
if match is None:
- log.debug('location is instance of %s but no URL match' % basestring)
- raise InvalidLocationError(location)
- else:
- groups = match.groupdict()
- check_dict(groups)
- return _LocationBase.__new__(_cls, **groups)
+ # cdodge:
+ # check for a dropped slash near the i4x:// element of the location string. This can happen with some
+ # redirects (e.g. edx.org -> www.edx.org which I think happens in Nginx)
+ match = MISSING_SLASH_URL_RE.match(location)
+ if match is None:
+ log.debug('location is instance of %s but no URL match' % basestring)
+ raise InvalidLocationError(location)
+ groups = match.groupdict()
+ check_dict(groups)
+ return _LocationBase.__new__(_cls, **groups)
elif isinstance(location, (list, tuple)):
if len(location) not in (5, 6):
log.debug('location has wrong length')
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index a22fcdb5f6..e84267c1e7 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -42,6 +42,7 @@ class CapaFactory(object):
force_save_button=None,
attempts=None,
problem_state=None,
+ correct=False
):
"""
All parameters are optional, and are added to the created problem if specified.
@@ -58,6 +59,7 @@ class CapaFactory(object):
module.
attempts: also added to instance state. Will be converted to an int.
+ correct: if True, the problem will be initialized to be answered correctly.
"""
definition = {'data': CapaFactory.sample_problem_xml, }
location = Location(["i4x", "edX", "capa_test", "problem",
@@ -81,10 +83,19 @@ class CapaFactory(object):
instance_state_dict = {}
if problem_state is not None:
instance_state_dict = problem_state
+
if attempts is not None:
# converting to int here because I keep putting "0" and "1" in the tests
# since everything else is a string.
instance_state_dict['attempts'] = int(attempts)
+
+ if correct:
+ # TODO: make this actually set an answer of 3.14, and mark it correct
+ #instance_state_dict['student_answers'] = {}
+ #instance_state_dict['correct_map'] = {}
+ pass
+
+
if len(instance_state_dict) > 0:
instance_state = json.dumps(instance_state_dict)
else:
@@ -94,13 +105,16 @@ class CapaFactory(object):
definition, descriptor,
instance_state, None, metadata=metadata)
+ if correct:
+ # TODO: probably better to actually set the internal state properly, but...
+ module.get_score = lambda: {'score': 1, 'total': 1}
+
return module
class CapaModuleTest(unittest.TestCase):
-
def setUp(self):
now = datetime.datetime.now()
day_delta = datetime.timedelta(days=1)
@@ -120,6 +134,18 @@ class CapaModuleTest(unittest.TestCase):
self.assertNotEqual(module.url_name, other_module.url_name,
"Factory should be creating unique names for each problem")
+
+ def test_correct(self):
+ """
+ Check that the factory creates correct and incorrect problems properly.
+ """
+ module = CapaFactory.create()
+ self.assertEqual(module.get_score()['score'], 0)
+
+ other_module = CapaFactory.create(correct=True)
+ self.assertEqual(other_module.get_score()['score'], 1)
+
+
def test_showanswer_default(self):
"""
Make sure the show answer logic does the right thing.
@@ -178,7 +204,7 @@ class CapaModuleTest(unittest.TestCase):
for everyone--e.g. after due date + grace period.
"""
- # can see after attempts used up, even with due date in the future
+ # can't see after attempts used up, even with due date in the future
used_all_attempts = CapaFactory.create(showanswer='past_due',
max_attempts="1",
attempts="1",
@@ -209,3 +235,50 @@ class CapaModuleTest(unittest.TestCase):
due=self.yesterday_str,
graceperiod=self.two_day_delta_str)
self.assertFalse(still_in_grace.answer_available())
+
+ def test_showanswer_finished(self):
+ """
+ With showanswer="finished" should show answer after the problem is closed,
+ or after the answer is correct.
+ """
+
+ # can see after attempts used up, even with due date in the future
+ used_all_attempts = CapaFactory.create(showanswer='finished',
+ max_attempts="1",
+ attempts="1",
+ due=self.tomorrow_str)
+ self.assertTrue(used_all_attempts.answer_available())
+
+
+ # can see after due date
+ past_due_date = CapaFactory.create(showanswer='finished',
+ max_attempts="1",
+ attempts="0",
+ due=self.yesterday_str)
+ self.assertTrue(past_due_date.answer_available())
+
+
+ # can't see because attempts left and wrong
+ attempts_left_open = CapaFactory.create(showanswer='finished',
+ max_attempts="1",
+ attempts="0",
+ due=self.tomorrow_str)
+ self.assertFalse(attempts_left_open.answer_available())
+
+ # _can_ see because attempts left and right
+ correct_ans = CapaFactory.create(showanswer='finished',
+ max_attempts="1",
+ attempts="0",
+ due=self.tomorrow_str,
+ correct=True)
+ self.assertTrue(correct_ans.answer_available())
+
+
+ # Can see even though grace period hasn't expired, because have no more
+ # attempts.
+ still_in_grace = CapaFactory.create(showanswer='finished',
+ max_attempts="1",
+ attempts="1",
+ due=self.yesterday_str,
+ graceperiod=self.two_day_delta_str)
+ self.assertTrue(still_in_grace.answer_available())
diff --git a/common/static/coffee/src/discussion/discussion.coffee b/common/static/coffee/src/discussion/discussion.coffee
index 068cde3de4..9cee068b74 100644
--- a/common/static/coffee/src/discussion/discussion.coffee
+++ b/common/static/coffee/src/discussion/discussion.coffee
@@ -39,6 +39,8 @@ if Backbone?
url = DiscussionUtil.urlFor 'threads'
when 'followed'
url = DiscussionUtil.urlFor 'followed_threads', options.user_id
+ if options['group_id']
+ data['group_id'] = options['group_id']
data['sort_key'] = sort_options.sort_key || 'date'
data['sort_order'] = sort_options.sort_order || 'desc'
DiscussionUtil.safeAjax
diff --git a/common/static/coffee/src/discussion/discussion_module_view.coffee b/common/static/coffee/src/discussion/discussion_module_view.coffee
index 63bd6bc733..4e4c2d1e7a 100644
--- a/common/static/coffee/src/discussion/discussion_module_view.coffee
+++ b/common/static/coffee/src/discussion/discussion_module_view.coffee
@@ -70,10 +70,21 @@ if Backbone?
DiscussionUtil.loadRoles(response.roles)
allow_anonymous = response.allow_anonymous
allow_anonymous_to_peers = response.allow_anonymous_to_peers
+ cohorts = response.cohorts
# $elem.html("Hide Discussion")
@discussion = new Discussion()
@discussion.reset(response.discussion_data, {silent: false})
- $discussion = $(Mustache.render $("script#_inline_discussion").html(), {'threads':response.discussion_data, 'discussionId': discussionId, 'allow_anonymous_to_peers': allow_anonymous_to_peers, 'allow_anonymous': allow_anonymous})
+
+ #use same discussion template but different thread templated
+ #determined in the coffeescript based on whether or not there's a
+ #group id
+
+ if response.is_cohorted
+ source = "script#_inline_discussion_cohorted"
+ else
+ source = "script#_inline_discussion"
+
+ $discussion = $(Mustache.render $(source).html(), {'threads':response.discussion_data, 'discussionId': discussionId, 'allow_anonymous_to_peers': allow_anonymous_to_peers, 'allow_anonymous': allow_anonymous, 'cohorts':cohorts})
if @$('section.discussion').length
@$('section.discussion').replaceWith($discussion)
else
diff --git a/common/static/coffee/src/discussion/views/discussion_thread_list_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_list_view.coffee
index c5f66c87ec..8364963218 100644
--- a/common/static/coffee/src/discussion/views/discussion_thread_list_view.coffee
+++ b/common/static/coffee/src/discussion/views/discussion_thread_list_view.coffee
@@ -9,6 +9,7 @@ if Backbone?
"click .browse-topic-drop-search-input": "ignoreClick"
"click .post-list .list-item a": "threadSelected"
"click .post-list .more-pages a": "loadMorePages"
+ "change .cohort-options": "chooseCohort"
'keyup .browse-topic-drop-search-input': DiscussionFilter.filterDrop
initialize: ->
@@ -128,10 +129,20 @@ if Backbone?
switch @mode
when 'search'
options.search_text = @current_search
+ if @group_id
+ options.group_id = @group_id
when 'followed'
options.user_id = window.user.id
+ options.group_id = "all"
when 'commentables'
options.commentable_ids = @discussionIds
+ if @group_id
+ options.group_id = @group_id
+ when 'all'
+ if @group_id
+ options.group_id = @group_id
+
+
@collection.retrieveAnotherPage(@mode, options, {sort_key: @sortBy})
renderThread: (thread) =>
@@ -263,13 +274,25 @@ if Backbone?
if discussionId == "#all"
@discussionIds = ""
@$(".post-search-field").val("")
+ @$('.cohort').show()
@retrieveAllThreads()
else if discussionId == "#following"
@retrieveFollowed(event)
+ @$('.cohort').hide()
else
discussionIds = _.map item.find(".board-name[data-discussion_id]"), (board) -> $(board).data("discussion_id").id
- @retrieveDiscussions(discussionIds)
-
+
+ if $(event.target).attr('cohorted') == "True"
+ @retrieveDiscussions(discussionIds, "function(){$('.cohort').show();}")
+ else
+ @retrieveDiscussions(discussionIds, "function(){$('.cohort').hide();}")
+
+ chooseCohort: (event) ->
+ @group_id = @$('.cohort-options :selected').val()
+ @collection.current_page = 0
+ @collection.reset()
+ @loadMorePages(event)
+
retrieveDiscussion: (discussion_id, callback=null) ->
url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id)
DiscussionUtil.safeAjax
diff --git a/common/static/coffee/src/discussion/views/discussion_thread_view_inline.coffee b/common/static/coffee/src/discussion/views/discussion_thread_view_inline.coffee
index 7dab9ae342..e648955d08 100644
--- a/common/static/coffee/src/discussion/views/discussion_thread_view_inline.coffee
+++ b/common/static/coffee/src/discussion/views/discussion_thread_view_inline.coffee
@@ -16,7 +16,10 @@ if Backbone?
@$delegateElement = @$local
render: ->
- @template = DiscussionUtil.getTemplate("_inline_thread")
+ if @model.has('group_id')
+ @template = DiscussionUtil.getTemplate("_inline_thread_cohorted")
+ else
+ @template = DiscussionUtil.getTemplate("_inline_thread")
if not @model.has('abbreviatedBody')
@abbreviateBody()
diff --git a/common/static/coffee/src/discussion/views/new_post_inline_vew.coffee b/common/static/coffee/src/discussion/views/new_post_inline_vew.coffee
index ed5ee13919..ffd43ff7bf 100644
--- a/common/static/coffee/src/discussion/views/new_post_inline_vew.coffee
+++ b/common/static/coffee/src/discussion/views/new_post_inline_vew.coffee
@@ -25,6 +25,7 @@ if Backbone?
event.preventDefault()
title = @$(".new-post-title").val()
body = @$(".new-post-body").find(".wmd-input").val()
+ group = @$(".new-post-group option:selected").attr("value")
# TODO tags: commenting out til we know what to do with them
#tags = @$(".new-post-tags").val()
@@ -45,6 +46,7 @@ if Backbone?
data:
title: title
body: body
+ group_id: group
# TODO tags: commenting out til we know what to do with them
#tags: tags
diff --git a/common/static/coffee/src/discussion/views/new_post_view.coffee b/common/static/coffee/src/discussion/views/new_post_view.coffee
index 1c49fdbc8e..606e4f30d7 100644
--- a/common/static/coffee/src/discussion/views/new_post_view.coffee
+++ b/common/static/coffee/src/discussion/views/new_post_view.coffee
@@ -14,8 +14,14 @@ if Backbone?
@setSelectedTopic()
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
+
@$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
-
+
+ if @$($(".topic_menu li a")[0]).attr('cohorted') != "True"
+ $('.choose-cohort').hide();
+
+
+
events:
"submit .new-post-form": "createPost"
"click .topic_dropdown_button": "toggleTopicDropdown"
@@ -65,6 +71,11 @@ if Backbone?
@topicText = @getFullTopicName($target)
@topicId = $target.data('discussion_id')
@setSelectedTopic()
+ if $target.attr('cohorted') == "True"
+ $('.choose-cohort').show();
+ else
+ $('.choose-cohort').hide();
+
setSelectedTopic: ->
@dropdownButton.html(@fitName(@topicText) + ' ▾')
@@ -116,6 +127,7 @@ if Backbone?
title = @$(".new-post-title").val()
body = @$(".new-post-body").find(".wmd-input").val()
tags = @$(".new-post-tags").val()
+ group = @$(".new-post-group option:selected").attr("value")
anonymous = false || @$("input.discussion-anonymous").is(":checked")
anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked")
@@ -137,6 +149,7 @@ if Backbone?
anonymous: anonymous
anonymous_to_peers: anonymous_to_peers
auto_subscribe: follow
+ group_id: group
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
success: (response, textStatus) =>
# TODO: Move this out of the callback, this makes it feel sluggish
diff --git a/common/static/js/course_groups/cohorts.js b/common/static/js/course_groups/cohorts.js
index 97837c6bac..2c9904ef40 100644
--- a/common/static/js/course_groups/cohorts.js
+++ b/common/static/js/course_groups/cohorts.js
@@ -144,6 +144,7 @@ var CohortManager = (function ($) {
$(".remove", tr).html('remove')
.click(function() {
remove_user_from_cohort(item.username, current_cohort_id, tr);
+ return false;
});
detail_users.append(tr);
@@ -217,6 +218,7 @@ var CohortManager = (function ($) {
show_cohorts_button.click(function() {
state = state_summary;
render();
+ return false;
});
add_cohort_input.change(function() {
@@ -231,12 +233,14 @@ var CohortManager = (function ($) {
var add_url = url + '/add';
data = {'name': add_cohort_input.val()}
$.post(add_url, data).done(added_cohort);
+ return false;
});
add_members_button.click(function() {
var add_url = detail_url + '/add';
data = {'users': users_area.val()}
$.post(add_url, data).done(added_users);
+ return false;
});
diff --git a/distribute-0.6.32.tar.gz b/distribute-0.6.32.tar.gz
new file mode 100644
index 0000000000..2438db60fa
Binary files /dev/null and b/distribute-0.6.32.tar.gz differ
diff --git a/distribute-0.6.34.tar.gz b/distribute-0.6.34.tar.gz
new file mode 100644
index 0000000000..4e91b3af62
Binary files /dev/null and b/distribute-0.6.34.tar.gz differ
diff --git a/doc/xml-format.md b/doc/xml-format.md
index c59db690a1..f387de1f52 100644
--- a/doc/xml-format.md
+++ b/doc/xml-format.md
@@ -277,9 +277,11 @@ Supported fields at the course level:
* "show_calculator" (value "Yes" if desired)
* "days_early_for_beta" -- number of days (floating point ok) early that students in the beta-testers group get to see course content. Can also be specified for any other course element, and overrides values set at higher levels.
* "cohort_config" : dictionary with keys
- - "cohorted" : boolean. Set to true if this course uses student cohorts. If so, all inline discussions are automatically cohorted, and top-level discussion topics are configurable with an optional 'cohorted': bool parameter (with default value false).
- - "cohorted_discussions": list of discussions that should be cohorted.
- - ... more to come. ('auto_cohort', how to auto cohort, etc)
+ - "cohorted" : boolean. Set to true if this course uses student cohorts. If so, all inline discussions are automatically cohorted, and top-level discussion topics are configurable via the cohorted_discussions list. Default is not cohorted).
+ - "cohorted_discussions": list of discussions that should be cohorted. Any not specified in this list are not cohorted.
+ - "auto_cohort": Truthy.
+ - "auto_cohort_groups": ["group name 1", "group name 2", ...]
+ - If cohorted and auto_cohort is true, automatically put each student into a random group from the auto_cohort_groups list, creating the group if needed.
* TODO: there are others
diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py
index 7ca00cb37c..e4e5ce1550 100644
--- a/lms/djangoapps/django_comment_client/base/views.py
+++ b/lms/djangoapps/django_comment_client/base/views.py
@@ -91,23 +91,31 @@ def create_thread(request, course_id, commentable_id):
'user_id': request.user.id,
})
+
+ user = cc.User.from_django_user(request.user)
+ #kevinchugh because the new requirement is that all groups will be determined
+ #by the group id in the request this all goes away
+
# Cohort the thread if the commentable is cohorted.
- if is_commentable_cohorted(course_id, commentable_id):
- user_group_id = get_cohort_id(request.user, course_id)
+ #if is_commentable_cohorted(course_id, commentable_id):
+ # user_group_id = get_cohort_id(user, course_id)
# TODO (vshnayder): once we have more than just cohorts, we'll want to
# change this to a single get_group_for_user_and_commentable function
# that can do different things depending on the commentable_id
- if cached_has_permission(request.user, "see_all_cohorts", course_id):
+ # if cached_has_permission(request.user, "see_all_cohorts", course_id):
# admins can optionally choose what group to post as
- group_id = post.get('group_id', user_group_id)
- else:
+ # group_id = post.get('group_id', user_group_id)
+ # else:
# regular users always post with their own id.
- group_id = user_group_id
-
- thread.update_attributes(group_id=group_id)
-
+ # group_id = user_group_id
+ group_id = post.get('group_id')
+ if group_id:
+ thread.update_attributes(group_id=group_id)
+
+ log.debug("Saving thread %r", thread.attributes)
thread.save()
+
if post.get('auto_subscribe', 'false').lower() == 'true':
user = cc.User.from_django_user(request.user)
user.follow(thread)
diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py
index 70d9f40fcf..efacbc1b4e 100644
--- a/lms/djangoapps/django_comment_client/forum/views.py
+++ b/lms/djangoapps/django_comment_client/forum/views.py
@@ -11,12 +11,12 @@ 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 get_cohort_id
+from course_groups.cohorts import is_course_cohorted, get_cohort_id, is_commentable_cohorted, get_cohorted_commentables, get_cohort, get_course_cohorts, get_cohort_by_id
from courseware.access import has_access
from urllib import urlencode
from operator import methodcaller
-from django_comment_client.permissions import check_permissions_by_view
+from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
from django_comment_client.utils import (merge_dict, extract, strip_none,
strip_blank, get_courseware_context)
@@ -50,22 +50,38 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
if not request.GET.get('sort_key'):
# If the user did not select a sort key, use their last used sort key
- user = cc.User.from_django_user(request.user)
- user.retrieve()
+ cc_user = cc.User.from_django_user(request.user)
+ cc_user.retrieve()
# TODO: After the comment service is updated this can just be user.default_sort_key because the service returns the default value
- default_query_params['sort_key'] = user.get('default_sort_key') or default_query_params['sort_key']
+ default_query_params['sort_key'] = cc_user.get('default_sort_key') or default_query_params['sort_key']
else:
# If the user clicked a sort key, update their default sort key
- user = cc.User.from_django_user(request.user)
- user.default_sort_key = request.GET.get('sort_key')
- user.save()
+ cc_user = cc.User.from_django_user(request.user)
+ cc_user.default_sort_key = request.GET.get('sort_key')
+ cc_user.save()
- #if the course-user is cohorted, then add the group id
- group_id = get_cohort_id(user, course_id)
+ #there are 2 dimensions to consider when executing a search with respect to group id
+ #is user a moderator
+ #did the user request a group
+
+ #if the user requested a group explicitly, give them that group, othewrise, if mod, show all, else if student, use cohort
+
+ group_id = request.GET.get('group_id')
+
+ if group_id == "all":
+ group_id = None
+
+ if not group_id:
+ if not cached_has_permission(request.user, "see_all_cohorts", course_id):
+ group_id = get_cohort_id(request.user, course_id)
+
if group_id:
default_query_params["group_id"] = group_id
+
+ #so by default, a moderator sees all items, and a student sees his cohort
+
query_params = merge_dict(default_query_params,
strip_none(extract(request.GET,
['page', 'sort_key',
@@ -73,6 +89,15 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
'tags', 'commentable_ids'])))
threads, page, num_pages = cc.Thread.search(query_params)
+
+ #now add the group name if the thread has a group id
+ for thread in threads:
+ if thread.get('group_id'):
+ thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name
+ thread['group_string'] = "This post visible only to Group %s." % (thread['group_name'])
+ else:
+ thread['group_name'] = ""
+ thread['group_string'] = "This post visible to everyone."
query_params['page'] = page
query_params['num_pages'] = num_pages
@@ -81,6 +106,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
def inline_discussion(request, course_id, discussion_id):
+
"""
Renders JSON for DiscussionModules
"""
@@ -89,7 +115,8 @@ def inline_discussion(request, course_id, discussion_id):
try:
threads, query_params = get_threads(request, course_id, discussion_id, per_page=INLINE_THREADS_PER_PAGE)
- user_info = cc.User.from_django_user(request.user).to_dict()
+ cc_user = cc.User.from_django_user(request.user)
+ user_info = cc_user.to_dict()
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
# TODO (vshnayder): since none of this code seems to be aware of the fact that
# sometimes things go wrong, I suspect that the js client is also not
@@ -101,7 +128,38 @@ def inline_discussion(request, course_id, discussion_id):
allow_anonymous = course.metadata.get("allow_anonymous", True)
allow_anonymous_to_peers = course.metadata.get("allow_anonymous_to_peers", False)
+
+ #since inline is all one commentable, only show or allow the choice of cohorts
+ #if the commentable is cohorted, otherwise everything is not cohorted
+ #and no one has the option of choosing a cohort
+ is_cohorted = is_course_cohorted(course_id) and is_commentable_cohorted(course_id, discussion_id)
+ cohorts_list = list()
+
+ if is_cohorted:
+ cohorts_list.append({'name':'All Groups','id':None})
+
+ #if you're a mod, send all cohorts and let you pick
+ if cached_has_permission(request.user, "see_all_cohorts", course_id):
+ cohorts = get_course_cohorts(course_id)
+ for c in cohorts:
+ cohorts_list.append({'name':c.name, 'id':c.id})
+
+ else:
+ #otherwise, just make a dictionary of two
+ user_cohort = get_cohort(cc_user, course_id)
+ if user_cohort:
+ user_cohort_name = user_cohort.name
+ user_cohort_id = user_cohort.id
+ else:
+ user_cohort_name = user_cohort_id = None
+
+ if user_cohort:
+ cohorts_list.append({'name':user_cohort_name, 'id':user_cohort_id})
+ else:
+ cohorts_list = None
+
+
return utils.JsonResponse({
'discussion_data': map(utils.safe_content, threads),
'user_info': user_info,
@@ -111,11 +169,14 @@ def inline_discussion(request, course_id, discussion_id):
'roles': utils.get_role_ids(course_id),
'allow_anonymous_to_peers': allow_anonymous_to_peers,
'allow_anonymous': allow_anonymous,
+ 'cohorts': cohorts_list,
+ 'is_cohorted': is_cohorted
})
@login_required
def forum_form_discussion(request, course_id):
+
"""
Renders the main Discussion page, potentially filtered by a search query
"""
@@ -129,7 +190,8 @@ def forum_form_discussion(request, course_id):
log.error("Error loading forum discussion threads: %s" % str(err))
raise Http404
- user_info = cc.User.from_django_user(request.user).to_dict()
+ user = cc.User.from_django_user(request.user)
+ user_info = user.to_dict()
annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
@@ -154,6 +216,10 @@ def forum_form_discussion(request, course_id):
#trending_tags = cc.search_trending_tags(
# course_id,
#)
+ cohorts = get_course_cohorts(course_id)
+ cohorted_commentables = get_cohorted_commentables(course_id)
+
+ user_cohort_id = get_cohort_id(request.user, course_id)
context = {
'csrf': csrf(request)['csrf_token'],
@@ -168,14 +234,18 @@ def forum_form_discussion(request, course_id):
'course_id': course.id,
'category_map': category_map,
'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict),
+ 'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id),
+ 'cohorts': cohorts,
+ 'user_cohort': user_cohort_id,
+ 'cohorted_commentables': cohorted_commentables,
+ 'is_course_cohorted': is_course_cohorted(course_id)
}
# print "start rendering.."
+
return render_to_response('discussion/index.html', context)
-
@login_required
def single_thread(request, course_id, discussion_id, thread_id):
-
course = get_course_with_access(request.user, course_id, 'load')
cc_user = cc.User.from_django_user(request.user)
user_info = cc_user.to_dict()
@@ -189,7 +259,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
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
@@ -219,6 +289,8 @@ def single_thread(request, course_id, discussion_id, thread_id):
courseware_context = get_courseware_context(thread, course)
if courseware_context:
thread.update(courseware_context)
+ if thread.get('group_id') and not thread.get('group_name'):
+ thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name
threads = [utils.safe_content(thread) for thread in threads]
@@ -232,8 +304,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
# course_id,
#)
-
annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
+
+ cohorts = get_course_cohorts(course_id)
+ cohorted_commentables = get_cohorted_commentables(course_id)
+ user_cohort = get_cohort_id(request.user, course_id)
context = {
'discussion_id': discussion_id,
@@ -250,6 +325,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
'category_map': category_map,
'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict),
'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),
+ 'cohorts': cohorts,
+ 'user_cohort': get_cohort_id(request.user, course_id),
+ 'cohorted_commentables': cohorted_commentables
}
return render_to_response('discussion/single_thread.html', context)
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index 877c730539..dde219294c 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -430,7 +430,7 @@ 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',
+ 'read', 'group_id', 'group_name', 'group_string'
]
if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False):
diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py
index 912ae1af18..ca607d3ff3 100644
--- a/lms/lib/comment_client/thread.py
+++ b/lms/lib/comment_client/thread.py
@@ -11,12 +11,12 @@ 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'
+ 'highlighted_body', 'endorsed', 'read', 'group_id', 'group_name'
]
updatable_fields = [
'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
- 'closed', 'tags', 'user_id', 'commentable_id', 'group_id'
+ 'closed', 'tags', 'user_id', 'commentable_id', 'group_id', 'group_name'
]
initializable_fields = updatable_fields
diff --git a/lms/lib/symmath/symmath_check.py b/lms/lib/symmath/symmath_check.py
index d386ea6b06..611948d030 100644
--- a/lms/lib/symmath/symmath_check.py
+++ b/lms/lib/symmath/symmath_check.py
@@ -215,7 +215,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
fans = None
# do a numerical comparison if both expected and answer are numbers
- if (hasattr(fexpect, 'is_number') and fexpect.is_number and fans
+ if (hasattr(fexpect, 'is_number') and fexpect.is_number
and hasattr(fans, 'is_number') and fans.is_number):
if abs(abs(fans - fexpect) / fexpect) < threshold:
return {'ok': True, 'msg': msg}
diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss
index 809c968fe6..e5134837fe 100644
--- a/lms/static/sass/_discussion.scss
+++ b/lms/static/sass/_discussion.scss
@@ -169,6 +169,12 @@ body.discussion {
}
}
+ .form-group-label {
+ display: block;
+ padding-top: 5px;
+ color:#fff;
+ }
+
.topic_dropdown_button {
position: relative;
z-index: 1000;
@@ -181,7 +187,7 @@ body.discussion {
.drop-arrow {
float: right;
color: #999;
- line-height: 36px;
+ line-height: 37px;
}
}
@@ -971,7 +977,8 @@ body.discussion {
}
.sort-bar {
- height: 27px;
+ height: auto;
+ min-height: 27px;
border-bottom: 1px solid #a3a3a3;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: #aeaeae;
@@ -1020,6 +1027,18 @@ body.discussion {
}
}
+ .group-filter-label {
+ width: 40px;
+ margin-left:10px;
+ }
+
+ .group-filter-select {
+ margin: 5px 0px 5px 5px;
+ width: 80px;
+ font-size:10px;
+ background: transparent;
+ border-color: #ccc;
+ }
}
.post-list-wrapper {
@@ -1327,6 +1346,8 @@ body.discussion {
margin-left: 40px;
}
+
+
.post-tools {
@include clearfix;
margin-top: 15px;
@@ -1357,6 +1378,8 @@ body.discussion {
margin-bottom: 20px;
}
+
+
.responses {
list-style: none;
margin-top: 40px;
@@ -2412,3 +2435,11 @@ body.discussion {
.discussion-user-threads {
@extend .discussion-module
}
+
+
+.group-visibility-label {
+ font-size: 12px;
+ color:#000;
+ font-style: italic;
+ background-color:#fff;
+ }
\ No newline at end of file
diff --git a/lms/templates/discussion/_filter_dropdown.html b/lms/templates/discussion/_filter_dropdown.html
index 484ee05101..fef4abb11f 100644
--- a/lms/templates/discussion/_filter_dropdown.html
+++ b/lms/templates/discussion/_filter_dropdown.html
@@ -11,7 +11,7 @@
%def>
<%def name="render_entry(entries, entry)">
- ${entry}
+ ${entry}
%def>
<%def name="render_category(categories, category)">
@@ -30,7 +30,7 @@