Commit bbea8eb9 by Chris Dodge

Merge branch 'master' of github.com:MITx/mitx into fix/cdodge/dangling-verticals-on-delete

parents 2e64bc33 2df3fe93
......@@ -4,22 +4,8 @@ Namespace defining common fields used by Studio for all blocks
import datetime
from xblock.core import Namespace, Boolean, Scope, ModelType, String
class StringyBoolean(Boolean):
"""
Reads strings from JSON as booleans.
If the string is 'true' (case insensitive), then return True,
otherwise False.
JSON values that aren't strings are returned as is
"""
def from_json(self, value):
if isinstance(value, basestring):
return value.lower() == 'true'
return value
from xblock.core import Namespace, Scope, ModelType, String
from xmodule.fields import StringyBoolean
class DateTuple(ModelType):
......
......@@ -12,7 +12,7 @@ from external_auth.djangostore import DjangoOpenIDStore
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
from django.contrib.auth.models import User
from student.models import UserProfile
from student.models import UserProfile, TestCenterUser, TestCenterRegistration
from django.http import HttpResponse, HttpResponseRedirect
from django.utils.http import urlquote
......@@ -34,6 +34,12 @@ from openid.server.trustroot import TrustRoot
from openid.extensions import ax, sreg
import student.views as student_views
# Required for Pearson
from courseware.views import get_module_for_descriptor, jump_to
from courseware.model_data import ModelDataCache
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location
log = logging.getLogger("mitx.external_auth")
......@@ -551,7 +557,7 @@ def provider_login(request):
'nickname': user.username,
'email': user.email,
'fullname': user.username
}
}
# the request succeeded:
return provider_respond(server, openid_request, response, results)
......@@ -606,3 +612,140 @@ def provider_xrds(request):
# custom XRDS header necessary for discovery process
response['X-XRDS-Location'] = get_xrds_url('xrds', request)
return response
#-------------------
# Pearson
#-------------------
def course_from_id(course_id):
"""Return the CourseDescriptor corresponding to this course_id"""
course_loc = CourseDescriptor.id_to_location(course_id)
return modulestore().get_instance(course_id, course_loc)
@csrf_exempt
def test_center_login(request):
''' Log in students taking exams via Pearson
Takes a POST request that contains the following keys:
- code - a security code provided by Pearson
- clientCandidateID
- registrationID
- exitURL - the url that we redirect to once we're done
- vueExamSeriesCode - a code that indicates the exam that we're using
'''
# errors are returned by navigating to the error_url, adding a query parameter named "code"
# which contains the error code describing the exceptional condition.
def makeErrorURL(error_url, error_code):
log.error("generating error URL with error code {}".format(error_code))
return "{}?code={}".format(error_url, error_code)
# get provided error URL, which will be used as a known prefix for returning error messages to the
# Pearson shell.
error_url = request.POST.get("errorURL")
# TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson
# with the code we calculate for the same parameters.
if 'code' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode"))
code = request.POST.get("code")
# calculate SHA for query string
# TODO: figure out how to get the original query string, so we can hash it and compare.
if 'clientCandidateID' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID"))
client_candidate_id = request.POST.get("clientCandidateID")
# TODO: check remaining parameters, and maybe at least log if they're not matching
# expected values....
# registration_id = request.POST.get("registrationID")
# exit_url = request.POST.get("exitURL")
# find testcenter_user that matches the provided ID:
try:
testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
except TestCenterUser.DoesNotExist:
log.error("not able to find demographics for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"))
# find testcenter_registration that matches the provided exam code:
# Note that we could rely in future on either the registrationId or the exam code,
# or possibly both. But for now we know what to do with an ExamSeriesCode,
# while we currently have no record of RegistrationID values at all.
if 'vueExamSeriesCode' not in request.POST:
# we are not allowed to make up a new error code, according to Pearson,
# so instead of "missingExamSeriesCode", we use a valid one that is
# inaccurate but at least distinct. (Sigh.)
log.error("missing exam series code for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID"))
exam_series_code = request.POST.get('vueExamSeriesCode')
registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code)
if not registrations:
log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"))
# TODO: figure out what to do if there are more than one registrations....
# for now, just take the first...
registration = registrations[0]
course_id = registration.course_id
course = course_from_id(course_id) # assume it will be found....
if not course:
log.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"))
exam = course.get_test_center_exam(exam_series_code)
if not exam:
log.error("not able to find exam {} for course ID {} and cand ID {}".format(exam_series_code, course_id, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"))
location = exam.exam_url
log.info("proceeding with test of cand {} on exam {} for course {}: URL = {}".format(client_candidate_id, exam_series_code, course_id, location))
# check if the test has already been taken
timelimit_descriptor = modulestore().get_instance(course_id, Location(location))
if not timelimit_descriptor:
log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
timelimit_descriptor, depth=None)
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
timelimit_module_cache, course_id, position=None)
if not timelimit_module.category == 'timelimit':
log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
if timelimit_module and timelimit_module.has_ended:
log.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at))
return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"))
# check if we need to provide an accommodation:
time_accommodation_mapping = {'ET12ET': 'ADDHALFTIME',
'ET30MN': 'ADD30MIN',
'ETDBTM': 'ADDDOUBLE', }
time_accommodation_code = None
for code in registration.get_accommodation_codes():
if code in time_accommodation_mapping:
time_accommodation_code = time_accommodation_mapping[code]
if time_accommodation_code:
timelimit_module.accommodation_code = time_accommodation_code
log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code))
# UGLY HACK!!!
# Login assumes that authentication has occurred, and that there is a
# backend annotation on the user object, indicating which backend
# against which the user was authenticated. We're authenticating here
# against the registration entry, and assuming that the request given
# this information is correct, we allow the user to be logged in
# without a password. This could all be formalized in a backend object
# that does the above checking.
# TODO: (brian) create a backend class to do this.
# testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
login(request, testcenteruser.user)
# And start the test:
return jump_to(request, course_id, location)
......@@ -1073,132 +1073,6 @@ def accept_name_change(request):
return accept_name_change_by_id(int(request.POST['id']))
@csrf_exempt
def test_center_login(request):
# errors are returned by navigating to the error_url, adding a query parameter named "code"
# which contains the error code describing the exceptional condition.
def makeErrorURL(error_url, error_code):
log.error("generating error URL with error code {}".format(error_code))
return "{}?code={}".format(error_url, error_code)
# get provided error URL, which will be used as a known prefix for returning error messages to the
# Pearson shell.
error_url = request.POST.get("errorURL")
# TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson
# with the code we calculate for the same parameters.
if 'code' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingSecurityCode"))
code = request.POST.get("code")
# calculate SHA for query string
# TODO: figure out how to get the original query string, so we can hash it and compare.
if 'clientCandidateID' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID"))
client_candidate_id = request.POST.get("clientCandidateID")
# TODO: check remaining parameters, and maybe at least log if they're not matching
# expected values....
# registration_id = request.POST.get("registrationID")
# exit_url = request.POST.get("exitURL")
# find testcenter_user that matches the provided ID:
try:
testcenteruser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
except TestCenterUser.DoesNotExist:
log.error("not able to find demographics for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"))
# find testcenter_registration that matches the provided exam code:
# Note that we could rely in future on either the registrationId or the exam code,
# or possibly both. But for now we know what to do with an ExamSeriesCode,
# while we currently have no record of RegistrationID values at all.
if 'vueExamSeriesCode' not in request.POST:
# we are not allowed to make up a new error code, according to Pearson,
# so instead of "missingExamSeriesCode", we use a valid one that is
# inaccurate but at least distinct. (Sigh.)
log.error("missing exam series code for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID"))
exam_series_code = request.POST.get('vueExamSeriesCode')
# special case for supporting test user:
if client_candidate_id == "edX003671291147" and exam_series_code != '6002x001':
log.warning("test user {} using unexpected exam code {}, coercing to 6002x001".format(client_candidate_id, exam_series_code))
exam_series_code = '6002x001'
registrations = TestCenterRegistration.objects.filter(testcenter_user=testcenteruser, exam_series_code=exam_series_code)
if not registrations:
log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"))
# TODO: figure out what to do if there are more than one registrations....
# for now, just take the first...
registration = registrations[0]
course_id = registration.course_id
course = course_from_id(course_id) # assume it will be found....
if not course:
log.error("not able to find course from ID {} for cand ID {}".format(course_id, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"))
exam = course.get_test_center_exam(exam_series_code)
if not exam:
log.error("not able to find exam {} for course ID {} and cand ID {}".format(exam_series_code, course_id, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "incorrectCandidateTests"))
location = exam.exam_url
log.info("proceeding with test of cand {} on exam {} for course {}: URL = {}".format(client_candidate_id, exam_series_code, course_id, location))
# check if the test has already been taken
timelimit_descriptor = modulestore().get_instance(course_id, Location(location))
if not timelimit_descriptor:
log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
timelimit_descriptor, depth=None)
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
timelimit_module_cache, course_id, position=None)
if not timelimit_module.category == 'timelimit':
log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
if timelimit_module and timelimit_module.has_ended:
log.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at))
return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"))
# check if we need to provide an accommodation:
time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME',
'ET30MN' : 'ADD30MIN',
'ETDBTM' : 'ADDDOUBLE', }
time_accommodation_code = None
for code in registration.get_accommodation_codes():
if code in time_accommodation_mapping:
time_accommodation_code = time_accommodation_mapping[code]
# special, hard-coded client ID used by Pearson shell for testing:
if client_candidate_id == "edX003671291147":
time_accommodation_code = 'TESTING'
if time_accommodation_code:
timelimit_module.accommodation_code = time_accommodation_code
log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code))
# UGLY HACK!!!
# Login assumes that authentication has occurred, and that there is a
# backend annotation on the user object, indicating which backend
# against which the user was authenticated. We're authenticating here
# against the registration entry, and assuming that the request given
# this information is correct, we allow the user to be logged in
# without a password. This could all be formalized in a backend object
# that does the above checking.
# TODO: (brian) create a backend class to do this.
# testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
login(request, testcenteruser.user)
# And start the test:
return jump_to(request, course_id, location)
def _get_news(top=None):
"Return the n top news items on settings.RSS_URL"
......
......@@ -16,35 +16,13 @@ from .progress import Progress
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.core import Integer, Scope, String, Boolean, Object, Float
from .fields import Timedelta, Date
from xblock.core import Scope, String, Boolean, Object
from .fields import Timedelta, Date, StringyInteger, StringyFloat
from xmodule.util.date_utils import time_to_datetime
log = logging.getLogger("mitx.courseware")
class StringyInteger(Integer):
"""
A model type that converts from strings to integers when reading from json
"""
def from_json(self, value):
try:
return int(value)
except:
return None
class StringyFloat(Float):
"""
A model type that converts from string to floats when reading from json
"""
def from_json(self, value):
try:
return float(value)
except:
return None
# Generated this many different variants of problems with rerandomize=per_student
NUM_RANDOMIZATION_BINS = 20
......@@ -95,7 +73,6 @@ class CapaFields(object):
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
display_name = String(help="Display name for this module", scope=Scope.settings)
seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
markdown = String(help="Markdown source of this module", scope=Scope.settings)
......
......@@ -8,8 +8,7 @@ from .x_module import XModule
from xblock.core import Integer, Scope, String, Boolean, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple
from .fields import Date
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
from .fields import Date, StringyFloat
log = logging.getLogger("mitx.courseware")
......
......@@ -7,6 +7,8 @@ from xblock.core import ModelType
import datetime
import dateutil.parser
from xblock.core import Integer, Float, Boolean
log = logging.getLogger(__name__)
......@@ -81,3 +83,42 @@ class Timedelta(ModelType):
if cur_value > 0:
values.append("%d %s" % (cur_value, attr))
return ' '.join(values)
class StringyInteger(Integer):
"""
A model type that converts from strings to integers when reading from json.
If value does not parse as an int, returns None.
"""
def from_json(self, value):
try:
return int(value)
except:
return None
class StringyFloat(Float):
"""
A model type that converts from string to floats when reading from json.
If value does not parse as a float, returns None.
"""
def from_json(self, value):
try:
return float(value)
except:
return None
class StringyBoolean(Boolean):
"""
Reads strings from JSON as booleans.
If the string is 'true' (case insensitive), then return True,
otherwise False.
JSON values that aren't strings are returned as-is.
"""
def from_json(self, value):
if isinstance(value, basestring):
return value.lower() == 'true'
return value
from xblock.core import Integer, Float
class StringyFloat(Float):
"""
A model type that converts from string to floats when reading from json
"""
def from_json(self, value):
try:
return float(value)
except:
return None
......@@ -11,8 +11,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo
from xblock.core import Object, Integer, Boolean, String, Scope
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
from xmodule.fields import Date
from xmodule.fields import Date, StringyFloat
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
from open_ended_grading_classes import combined_open_ended_rubric
......
"""Tests for Date class defined in fields.py."""
"""Tests for classes defined in fields.py."""
import datetime
import unittest
from django.utils.timezone import UTC
from xmodule.fields import Date
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
import time
class DateTest(unittest.TestCase):
......@@ -78,3 +78,55 @@ class DateTest(unittest.TestCase):
DateTest.date.from_json("2012-12-31T23:00:01-01:00")),
"2013-01-01T00:00:01Z")
class StringyIntegerTest(unittest.TestCase):
def assertEquals(self, expected, arg):
self.assertEqual(expected, StringyInteger().from_json(arg))
def test_integer(self):
self.assertEquals(5, '5')
self.assertEquals(0, '0')
self.assertEquals(-1023, '-1023')
def test_none(self):
self.assertEquals(None, None)
self.assertEquals(None, 'abc')
self.assertEquals(None, '[1]')
self.assertEquals(None, '1.023')
class StringyFloatTest(unittest.TestCase):
def assertEquals(self, expected, arg):
self.assertEqual(expected, StringyFloat().from_json(arg))
def test_float(self):
self.assertEquals(.23, '.23')
self.assertEquals(5, '5')
self.assertEquals(0, '0.0')
self.assertEquals(-1023.22, '-1023.22')
def test_none(self):
self.assertEquals(None, None)
self.assertEquals(None, 'abc')
self.assertEquals(None, '[1]')
class StringyBooleanTest(unittest.TestCase):
def assertEquals(self, expected, arg):
self.assertEqual(expected, StringyBoolean().from_json(arg))
def test_false(self):
self.assertEquals(False, "false")
self.assertEquals(False, "False")
self.assertEquals(False, "")
self.assertEquals(False, "hahahahah")
def test_true(self):
self.assertEquals(True, "true")
self.assertEquals(True, "TruE")
def test_pass_through(self):
self.assertEquals(123, 123)
......@@ -20,7 +20,6 @@ log = logging.getLogger(__name__)
class VideoFields(object):
data = String(help="XML data for the problem", scope=Scope.content)
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
display_name = String(help="Display name for this module", scope=Scope.settings)
class VideoModule(VideoFields, XModule):
......
......@@ -88,32 +88,20 @@ 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)
'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id)
'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @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)
'pinThread' : -> DiscussionUtil.urlFor("pin_thread", @id)
'unPinThread' : -> DiscussionUtil.urlFor("un_pin_thread", @id)
......@@ -169,8 +157,6 @@ 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,9 +37,6 @@ 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,12 +18,8 @@ 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'])
......@@ -52,10 +48,6 @@ 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"
......@@ -80,7 +72,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
......@@ -99,48 +94,7 @@ 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,11 +276,6 @@ 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()
......@@ -326,12 +321,6 @@ 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,7 +3,6 @@ if Backbone?
events:
"click .discussion-vote": "toggleVote"
"click .discussion-flag-abuse": "toggleFlagAbuse"
"click .admin-pin": "togglePin"
"click .action-follow": "toggleFollowing"
"click .action-edit": "edit"
......@@ -26,7 +25,6 @@ if Backbone?
@delegateEvents()
@renderDogear()
@renderVoted()
@renderFlagged()
@renderPinned()
@renderAttrs()
@$("span.timeago").timeago()
......@@ -44,16 +42,6 @@ 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")
......@@ -68,7 +56,6 @@ if Backbone?
updateModelDetails: =>
@renderVoted()
@renderFlagged()
@renderPinned()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
......@@ -109,7 +96,6 @@ if Backbone?
if textStatus == 'success'
@model.set(response, {silent: true})
unvote: ->
window.user.unvote(@model)
url = @model.urlFor("unvote")
......@@ -121,7 +107,6 @@ if Backbone?
if textStatus == 'success'
@model.set(response, {silent: true})
edit: (event) ->
@trigger "thread:edit", event
......@@ -197,4 +182,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 }, abuse_flaggers:[], 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 }, 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())
......@@ -18,7 +11,6 @@ if Backbone?
@initLocal()
@delegateEvents()
@renderAttrs()
@renderFlagged()
@markAsStaff()
@$el.find(".timeago").timeago()
@convertMath()
......@@ -42,17 +34,3 @@ 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,7 +5,6 @@ if Backbone?
"click .action-endorse": "toggleEndorse"
"click .action-delete": "delete"
"click .action-edit": "edit"
"click .discussion-flag-abuse": "toggleFlagAbuse"
$: (selector) ->
@$el.find(selector)
......@@ -24,7 +23,6 @@ if Backbone?
if window.user.voted(@model)
@$(".vote-btn").addClass("is-cast")
@renderAttrs()
@renderFlagged()
@$el.find(".posted-details").timeago()
@convertMath()
@markAsStaff()
......@@ -72,7 +70,6 @@ if Backbone?
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
edit: (event) ->
@trigger "response:edit", event
......@@ -95,17 +92,3 @@ 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"), abuse_flaggers:[], user_id: window.user.get("id"), id:"unsaved")
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), user_id: window.user.get("id"), id:"unsaved")
view = @renderComment(comment)
@hideEditorChrome()
@trigger "comment:add", comment
......
......@@ -11,8 +11,6 @@ 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'),
......@@ -27,8 +25,7 @@ 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, get_course_by_id
from courseware.courses import get_course_with_access
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,71 +287,25 @@ 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()))
......@@ -498,11 +452,16 @@ 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
......@@ -513,7 +472,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,10 +9,9 @@ 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)
......@@ -80,7 +79,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', 'flagged'])))
'tags', 'commentable_ids'])))
threads, page, num_pages = cc.Thread.search(query_params)
......@@ -93,7 +92,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
......@@ -109,6 +108,7 @@ def inline_discussion(request, course_id, discussion_id):
"""
Renders JSON for DiscussionModules
"""
course = get_course_with_access(request.user, course_id, 'load')
try:
......@@ -219,7 +219,6 @@ 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,
......@@ -242,12 +241,19 @@ 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
......@@ -319,7 +325,6 @@ 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
......@@ -407,7 +412,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,11 +6,10 @@ 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)
......@@ -23,7 +22,8 @@ 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,6 +73,7 @@ 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")
......@@ -88,10 +89,6 @@ 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,3 +39,4 @@ class CloseThreadTextTest(TestCase):
self.assertEqual(mustache_helpers.close_thread_text(self.contentOpen), 'Close thread')
#########################################################################################
import time
from collections import defaultdict
import logging
import time
......@@ -105,12 +104,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"] = {}
......@@ -175,7 +174,8 @@ 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,7 +318,6 @@ 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', []):
......@@ -383,8 +382,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
......@@ -397,8 +396,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', 'group_id', 'group_name', 'group_string', 'pinned', 'abuse_flaggers'
'read', 'group_id', 'group_name', 'group_string', 'pinned'
]
if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False):
......
......@@ -224,9 +224,7 @@ FILE_UPLOAD_HANDLERS = (
PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
########################## PEARSON TESTING ###########################
MITX_FEATURES['ENABLE_PEARSON_HACK_TEST'] = True
PEARSON_TEST_USER = "pearsontest"
PEARSON_TEST_PASSWORD = "12345"
MITX_FEATURES['ENABLE_PEARSON_LOGIN'] = False
########################## ANALYTICS TESTING ########################
......
......@@ -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', 'abuse_flaggers'
'type', 'commentable_id',
]
updatable_fields = [
'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'closed',
'user_id', 'endorsed'
'user_id', 'endorsed',
]
initializable_fields = updatable_fields
......@@ -42,32 +42,6 @@ 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)
......@@ -75,11 +49,3 @@ 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,6 +29,7 @@ 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
......@@ -10,7 +11,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', 'abuse_flaggers'
'highlighted_body', 'endorsed', 'read', 'group_id', 'group_name', 'pinned'
]
updatable_fields = [
......@@ -26,13 +27,11 @@ 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:
......@@ -55,7 +54,6 @@ 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':
......@@ -68,11 +66,12 @@ 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.
......@@ -80,57 +79,23 @@ 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)
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)
self.update_attributes(request)
def _url_for_pin_thread(thread_id):
return "{prefix}/threads/{thread_id}/pin".format(prefix=settings.PREFIX, thread_id=thread_id)
return "{prefix}/threads/{thread_id}/pin".format(prefix=settings.PREFIX, thread_id=thread_id)
def _url_for_un_pin_thread(thread_id):
return "{prefix}/threads/{thread_id}/unpin".format(prefix=settings.PREFIX, thread_id=thread_id)
return "{prefix}/threads/{thread_id}/unpin".format(prefix=settings.PREFIX, thread_id=thread_id)
\ No newline at end of file
......@@ -95,7 +95,6 @@
body.discussion {
.new-post-form-errors {
display: none;
background: $error-red;
......@@ -1281,8 +1280,8 @@ body.discussion {
.discussion-article {
position: relative;
padding: 40px;
min-height: 468px;
min-height: 468px;
a {
word-wrap: break-word;
}
......@@ -1335,9 +1334,6 @@ body.discussion {
background-position: 0 0;
}
}
}
.discussion-post {
......@@ -2440,6 +2436,7 @@ body.discussion {
@extend .discussion-module
}
.group-visibility-label {
font-size: 12px;
color:#000;
......@@ -2494,39 +2491,4 @@ body.discussion {
.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
......@@ -33,14 +33,6 @@
<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,7 +3,6 @@
<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>
......@@ -31,8 +30,7 @@
<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) { %>"}
......@@ -47,10 +45,6 @@
</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>
......@@ -124,10 +118,7 @@
${"<% } 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 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>
<div class="response-local"><div class="response-body">${"<%- body %>"}</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>
......@@ -150,8 +141,6 @@
<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}" data-flag-moderator="${flag_moderator}">
<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}">
<div class="discussion-body">
<div class="sidebar"></div>
<div class="discussion-column">
......
......@@ -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}" data-flag-moderator="${flag_moderator}">
<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}">
<div class="discussion-body">
<div class="sidebar"></div>
<div class="discussion-column"></div>
......
......@@ -32,12 +32,6 @@ urlpatterns = ('',
url(r'^accept_name_change$', 'student.views.accept_name_change'),
url(r'^reject_name_change$', 'student.views.reject_name_change'),
url(r'^pending_name_changes$', 'student.views.pending_name_changes'),
url(r'^testcenter/login$', 'student.views.test_center_login'),
# url(r'^testcenter/login$', 'student.test_center_views.login'),
# url(r'^testcenter/logout$', 'student.test_center_views.logout'),
url(r'^event$', 'track.views.user_track'),
url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB?
......@@ -130,8 +124,6 @@ urlpatterns = ('',
if settings.PERFSTATS:
urlpatterns += (url(r'^reprofile$', 'perfstats.views.end_profile'),)
# Multicourse wiki (Note: wiki urls must be above the courseware ones because of
# the custom tab catch-all)
if settings.WIKI_ENABLED:
......@@ -144,8 +136,6 @@ if settings.WIKI_ENABLED:
# First we include views from course_wiki that we use to override the default views.
# They come first in the urlpatterns so they get resolved first
url('^wiki/create-root/$', 'course_wiki.views.root_create', name='root_create'),
url(r'^wiki/', include(wiki_pattern())),
url(r'^notify/', include(notify_pattern())),
......@@ -269,7 +259,7 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems/take_action_on_flags$',
'open_ended_grading.views.take_action_on_flags', name='open_ended_flagged_problems_take_action'),
# Cohorts management
# Cohorts management
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/cohorts$',
'course_groups.views.list_cohorts', name="cohorts"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/cohorts/add$',
......@@ -299,9 +289,8 @@ if settings.COURSEWARE_ENABLED:
# 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"),
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'):
......@@ -347,6 +336,9 @@ if settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'):
url(r'^openid/provider/xrds/$', 'external_auth.views.provider_xrds', name='openid-provider-xrds')
)
if settings.MITX_FEATURES.get('ENABLE_PEARSON_LOGIN', False):
urlpatterns += url(r'^testcenter/login$', 'external_auth.views.test_center_login'),
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
urlpatterns += (
url(r'^migrate/modules$', 'lms_migration.migrate.manage_modulestores'),
......
"""
Namespace that defines fields common to all blocks used in the LMS
"""
from xblock.core import Namespace, Boolean, Scope, String, Float
from xmodule.fields import Date, Timedelta
class StringyBoolean(Boolean):
"""
Reads strings from JSON as booleans.
'true' (case insensitive) return True, other strings return False
Other types are returned unchanged
"""
def from_json(self, value):
if isinstance(value, basestring):
return value.lower() == 'true'
return value
class StringyFloat(Float):
"""
Reads values as floats. If the value parses as a float, returns
that, otherwise returns None
"""
def from_json(self, value):
try:
return float(value)
except:
return None
from xblock.core import Namespace, Boolean, Scope, String
from xmodule.fields import Date, Timedelta, StringyFloat, StringyBoolean
class LmsNamespace(Namespace):
......
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