Commit d265c255 by John Hess

Merge branch 'master' of github.com:MITx/mitx

parents fbce4251 ae681a52
......@@ -27,4 +27,5 @@ lms/lib/comment_client/python
nosetests.xml
cover_html/
.idea/
.redcar/
chromedriver.log
\ No newline at end of file
10664: Locked by 10664 at Mon Feb 11 14:22:22 -0500 2013
---
cursor_positions: []
files_to_retain: 0
ce76efcea5f0a5b2238364f81d54f1d393853a1a
\ No newline at end of file
......@@ -68,10 +68,10 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
save_videosource: function(newsource) {
// newsource either is <video youtube="speed:key, *"/> 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();
......
......@@ -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
......@@ -65,6 +66,22 @@ def is_commentable_cohorted(course_id, commentable_id):
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):
"""
......
......@@ -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'
......
......@@ -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':
......
......@@ -379,6 +379,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):
"""
Return list of topic ids defined in course policy.
......
......@@ -23,6 +23,15 @@ URL_RE = re.compile("""
(@(?P<revision>[^/]+))?
""", re.VERBOSE)
MISSING_SLASH_URL_RE = re.compile("""
(?P<tag>[^:]+):/
(?P<org>[^/]+)/
(?P<course>[^/]+)/
(?P<category>[^/]+)/
(?P<name>[^@]+)
(@(?P<revision>[^/]+))?
""", 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,9 +173,13 @@ class Location(_LocationBase):
if isinstance(location, basestring):
match = URL_RE.match(location)
if match is None:
# 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)
else:
groups = match.groupdict()
check_dict(groups)
return _LocationBase.__new__(_cls, **groups)
......
......@@ -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())
......@@ -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
......
......@@ -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
......
......@@ -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,12 +274,24 @@ 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)
......
......@@ -16,6 +16,9 @@ if Backbone?
@$delegateElement = @$local
render: ->
if @model.has('group_id')
@template = DiscussionUtil.getTemplate("_inline_thread_cohorted")
else
@template = DiscussionUtil.getTemplate("_inline_thread")
if not @model.has('abbreviatedBody')
......
......@@ -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
......
......@@ -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) + ' <span class="drop-arrow">▾</span>')
......@@ -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
......
......@@ -144,6 +144,7 @@ var CohortManager = (function ($) {
$(".remove", tr).html('<a href="#">remove</a>')
.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;
});
......
......@@ -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
......
......@@ -92,22 +92,30 @@ def create_thread(request, course_id, commentable_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
# 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)
......
......@@ -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):
......
......@@ -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
......
......@@ -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}
......
......@@ -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
......@@ -11,7 +11,7 @@
</%def>
<%def name="render_entry(entries, entry)">
<li><a href="#"><span class="board-name" data-discussion_id='${json.dumps(entries[entry])}'>${entry}</span></a></li>
<li><a href="#"><span class="board-name" data-discussion_id='${json.dumps(entries[entry])}' cohorted = "${entries[entry]['id'] in cohorted_commentables}">${entry}</span></a></li>
</%def>
<%def name="render_category(categories, category)">
......@@ -30,7 +30,7 @@
<ul class="browse-topic-drop-menu">
<li>
<a href="#">
<span class="board-name" data-discussion_id='#all'>All</span>
<span class="board-name" data-discussion_id='#all'>Show All Discussions</span>
</a>
</li>
<li>
......
......@@ -12,6 +12,24 @@
%elif course.metadata.get("allow_anonymous_to_peers", False):
<input type="checkbox" name="anonymous_to_peers" class="discussion-anonymous-to-peers" id="new-post-anonymous-to-peers"><label for="new-post-anonymous-to-peers">post anonymously to classmates</label>
%endif
%if is_course_cohorted:
<div class="form-group-label choose-cohort">
Make visible to:
<select class="group-filter-select new-post-group" name = "group_id">
<option value="">All Groups</option>
%if is_moderator:
%for c in cohorts:
<option value="${c.id}">${c.name}</option>
%endfor
%else:
%if user_cohort:
<option value="${user_cohort}">My Cohort</option>
%endif
%endif
</select>
</div>
%endif
</div>
</div>
<div class="right-column">
......
......@@ -9,7 +9,7 @@
</%def>
<%def name="render_entry(entries, entry)">
<li><a href="#" class="topic" data-discussion_id="${entries[entry]['id']}">${entry}</a></li>
<li><a href="#" class="topic" data-discussion_id="${entries[entry]['id']}" cohorted = "${entries[entry]['id'] in cohorted_commentables}">${entry}</a></li>
</%def>
<%def name="render_category(categories, category)">
......@@ -21,13 +21,14 @@
</li>
</%def>
<article class="new-post-article">
<div class="inner-wrapper">
<form class="new-post-form">
<div class="left-column">
<label>Create new post about:</label>
<div class="form-topic-drop">
<a href="#" class="topic_dropdown_button">All<span class="drop-arrow"></span></a>
<a href="#" class="topic_dropdown_button">Show All Discussions<span class="drop-arrow"></span></a>
<div class="topic_menu_wrapper">
<div class="topic_menu_search">
<input type="text" class="form-topic-drop-search-input" placeholder="filter topics">
......@@ -45,6 +46,24 @@
%elif course.metadata.get("allow_anonymous_to_peers", False):
<input type="checkbox" name="anonymous_to_peers" class="discussion-anonymous-to-peers" id="new-post-anonymous-to-peers"><label for="new-post-anonymous-to-peers">post anonymously to classmates</label>
%endif
%if is_course_cohorted:
<div class="form-group-label choose-cohort">
Make visible to:
<select class="group-filter-select new-post-group" name = "group_id">
<option value="">All Groups</option>
%if is_moderator:
%for c in cohorts:
<option value="${c.id}">${c.name}</option>
%endfor
%else:
%if user_cohort:
<option value="${user_cohort}">My Cohort</option>
%endif
%endif
</select>
</div>
%endif
</div>
</div>
<div class="right-column">
......
......@@ -4,7 +4,12 @@
<article class="discussion-article" data-id="${discussion_id| h}">
<a href="#" class="dogear"></a>
<div class="discussion-post">
<header>
%if thread['group_id']
<div class="group-visibility-label">This post visible only to group ${cohort_dictionary[thread['group_id']]}. </div>
%endif
<a href="#" class="vote-btn discussion-vote discussion-vote-up"><span class="plus-icon">+</span> <span class='votes-count-number'>${thread['votes']['up_count']}</span></a>
<h1>${thread['title']}</h1>
<p class="posted-details">
......
......@@ -2,7 +2,7 @@
<div class="browse-search">
<div class="browse is-open">
<a href="#" class="browse-topic-drop-icon"></a>
<a href="#" class="browse-topic-drop-btn"><span class="current-board">All</span> <span class="drop-arrow">▾</span></a>
<a href="#" class="browse-topic-drop-btn"><span class="current-board">Show All Discussions</span> <span class="drop-arrow">▾</span></a>
</div>
<%include file="_filter_dropdown.html" />
<div class="search">
......@@ -18,6 +18,17 @@
<li><a href="#" data-sort="votes">votes</a></li>
<li><a href="#" data-sort="comments">comments</a></li>
</ul>
%if is_course_cohorted and is_moderator:
<span class="group-filter-label cohort">Show:</span>
<select class="group-filter-select cohort-options cohort">
<option value="all">View All</option>
%for c in cohorts:
<option value="${c.id}">View as ${c.name}</option>
%endfor
</select>
%endif
</div>
<div class="post-list-wrapper">
<ul class="post-list">
......
......@@ -26,6 +26,10 @@
<div class="discussion-post">
<div><a href="javascript:void(0)" class="dogear action-follow" data-tooltip="follow"></a></div>
<header>
${"<% if (obj.group_id) { %>"}
<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>
<h1>${'<%- title %>'}</h1>
<p class="posted-details">
......
<div class="discussion-content local{{#content.roles}} role-{{name}}{{/content.roles}}">
CONTENT MUSTACHE
<div class="discussion-content-wrapper">
<div class="discussion-votes">
<a class="discussion-vote discussion-vote-up" href="javascript:void(0)" value="up">&#9650;</a>
......
<section class="discussion" data-discussion-id="{{discussionId}}">
<article class="new-post-article">
<span class="topic" data-discussion-id="{{discussionId}}" />
<div class="inner-wrapper">
......
<section class="discussion" data-discussion-id="{{discussionId}}">
<article class="new-post-article">
<span class="topic" data-discussion-id="{{discussionId}}" />
<div class="inner-wrapper">
<div class="new-post-form-errors">
</div>
<form class="new-post-form">
<div class="form-row">
<input type="text" class="new-post-title" name="title" placeholder="Title">
</div>
<div class="form-row">
<div class="new-post-body" name="body" placeholder="Enter your question or comment&hellip;"></div>
<!---<div class="new-post-preview"><span class="new-post-preview-label">Preview</span></div>-->
</div>
{{! TODO tags: Getting rid of tags for now. }}
{{!<div class="form-row">}}
{{! <input type="text" class="new-post-tags" name="tags" placeholder="Tags">}}
{{!</div>}}
<input type="submit" class="submit" value="Add post">
<a href="#" class="new-post-cancel">Cancel</a>
<div class="options">
<input type="checkbox" name="follow" class="discussion-follow" class="discussion-follow" id="new-post-follow" checked><label for="new-post-follow">follow this post</label>
<br>
{{#allow_anonymous}}
<input type="checkbox" name="anonymous" class="discussion-anonymous" id="new-post-anonymous"><label for="new-post-anonymous">post anonymously</label>
{{/allow_anonymous}}
{{#allow_anonymous_to_peers}}
<input type="checkbox" name="anonymous" class="discussion-anonymous-to-peers" id="new-post-anonymous-to-peers"><label for="new-post-anonymous-to-peers">post anonymously to classmates</label>
{{/allow_anonymous_to_peers}}
<div class="form-group-label choose-cohort">
Make visible to:
<select class="group-filter-select new-post-group" name = "group_id">
{{#cohorts}}
<option value="{{id}}">{{name}}</option>
{{/cohorts}}
</select>
</div>
</div>
</form>
</div>
</article>
<section class="threads">
{{#threads}}
<article class="discussion-thread" id="thread_{{id}}">
</article>
{{/threads}}
</section>
<section class="pagination">
</section>
</section>
<article class="discussion-article" data-id="{{id}}">
<div class="thread-content-wrapper"></div>
<ol class="responses post-extended-content">
......
<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">
<li class="loading"><div class="loading-animation"></div></li>
</ol>
<form class="local discussion-reply-new post-extended-content" data-id="{{id}}">
<h4>Post a response:</h4>
<ul class="discussion-errors"></ul>
<div class="reply-body" data-id="{{id}}"></div>
<div class="reply-post-control">
<a class="discussion-submit-post control-button" href="#">Submit</a>
</div>
</form>
<div class="local post-tools">
<a href="javascript:void(0)" class="expand-post">View discussion</a>
<a href="javascript:void(0)" class="collapse-post">Hide discussion</a>
</div>
</article>
\ No newline at end of file
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