diff --git a/.gitignore b/.gitignore index e7b0b16be8..87a0778a6f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,13 +31,4 @@ cover_html/ chromedriver.log /nbproject ghostdriver.log -/cms/doc/en/getting_started/ -/conf/locale/en -/conf/locale/fr -create-dev-env.hack.sh -distribute-0.6.36.tar.gz -i18n/googleTranslate.hack.py -i18n/mitx/conf/locale/fr/LC_MESSAGES/django.po -i18n/split.py -.gitignore - +node_modules diff --git a/cms/static/coffee/.gitignore b/cms/static/coffee/.gitignore index e114474f98..a6c7c2852d 100644 --- a/cms/static/coffee/.gitignore +++ b/cms/static/coffee/.gitignore @@ -1,3 +1 @@ *.js -descriptor -module diff --git a/cms/static/sass/.gitignore b/cms/static/sass/.gitignore index 62a745a9d7..b3a5267117 100644 --- a/cms/static/sass/.gitignore +++ b/cms/static/sass/.gitignore @@ -1,3 +1 @@ *.css -descriptor -module diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py index c9bb8f4c6e..1b509a14f4 100644 --- a/cms/xmodule_namespace.py +++ b/cms/xmodule_namespace.py @@ -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): diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index effae923b3..23b46aa803 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -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) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 2b359acc6a..eee9a3dc53 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -1082,132 +1082,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" diff --git a/common/lib/rooted_paths.py b/common/lib/rooted_paths.py index 9084768639..3339d6471d 100644 --- a/common/lib/rooted_paths.py +++ b/common/lib/rooted_paths.py @@ -8,7 +8,7 @@ def rooted_glob(root, glob): Uses glob2 globbing """ - return remove_root(root, glob2.glob('{root}/{glob}'.format(root=root, glob=glob))) + return remove_root(root, sorted(glob2.glob('{root}/{glob}'.format(root=root, glob=glob)))) def remove_root(root, paths): diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 9b0cc44ab4..4143345196 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -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) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 120e4f743a..239adcaa41 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -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") diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py index bb85714252..3d56b7941e 100644 --- a/common/lib/xmodule/xmodule/fields.py +++ b/common/lib/xmodule/xmodule/fields.py @@ -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 diff --git a/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee b/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee index d59e936b8c..b6c562c88a 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee @@ -37,7 +37,7 @@ xdescribe 'VideoPlayer', -> expect(window.VideoProgressSlider).toHaveBeenCalledWith el: $('.slider', @player.el) it 'create Youtube player', -> - expect(YT.Player).toHaveBeenCalledWith 'example' + expect(YT.Player).toHaveBeenCalledWith('example', { playerVars: controls: 0 wmode: 'transparent' @@ -48,6 +48,7 @@ xdescribe 'VideoPlayer', -> events: onReady: @player.onReady onStateChange: @player.onStateChange + }) it 'bind to video control play event', -> expect($(@player.control)).toHandleWith 'play', @player.play diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/xblock_field_types.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/xblock_field_types.py deleted file mode 100644 index 2dcb7a4cda..0000000000 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/xblock_field_types.py +++ /dev/null @@ -1,14 +0,0 @@ -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 - diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index db4514d0e0..35f2fa2d76 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -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 diff --git a/common/lib/xmodule/xmodule/static_content.py b/common/lib/xmodule/xmodule/static_content.py index c110d310eb..f433a9d6c9 100755 --- a/common/lib/xmodule/xmodule/static_content.py +++ b/common/lib/xmodule/xmodule/static_content.py @@ -68,7 +68,7 @@ def _write_styles(selector, output_root, classes): css_fragments[idx, filetype, fragment].add(class_.__name__) css_imports = defaultdict(set) for (idx, filetype, fragment), classes in sorted(css_fragments.items()): - fragment_name = "{idx}-{hash}.{type}".format( + fragment_name = "{idx:0=3d}-{hash}.{type}".format( idx=idx, hash=hashlib.md5(fragment).hexdigest(), type=filetype) @@ -102,7 +102,7 @@ def _write_js(output_root, classes): module_js = [] for idx, filetype, fragment in sorted(js_fragments): - path = output_root / "{idx}-{hash}.{type}".format( + path = output_root / "{idx:0=3d}-{hash}.{type}".format( idx=idx, hash=hashlib.md5(fragment).hexdigest(), type=filetype) diff --git a/common/lib/xmodule/xmodule/tests/test_fields.py b/common/lib/xmodule/xmodule/tests/test_fields.py index 7c8872efc1..9642f7c595 100644 --- a/common/lib/xmodule/xmodule/tests/test_fields.py +++ b/common/lib/xmodule/xmodule/tests/test_fields.py @@ -1,8 +1,8 @@ -"""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) + diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 2343d24a57..f902a9665b 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -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): diff --git a/common/static/coffee/src/discussion/content.coffee b/common/static/coffee/src/discussion/content.coffee index 6361a4b76e..00c34df686 100644 --- a/common/static/coffee/src/discussion/content.coffee +++ b/common/static/coffee/src/discussion/content.coffee @@ -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 diff --git a/common/static/coffee/src/discussion/discussion.coffee b/common/static/coffee/src/discussion/discussion.coffee index 5a52cd4de0..83e25e1da7 100644 --- a/common/static/coffee/src/discussion/discussion.coffee +++ b/common/static/coffee/src/discussion/discussion.coffee @@ -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'] diff --git a/common/static/coffee/src/discussion/utils.coffee b/common/static/coffee/src/discussion/utils.coffee index 5c2dea7e7c..41f52f1711 100644 --- a/common/static/coffee/src/discussion/utils.coffee +++ b/common/static/coffee/src/discussion/utils.coffee @@ -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] diff --git a/common/static/coffee/src/discussion/views/discussion_content_view.coffee b/common/static/coffee/src/discussion/views/discussion_content_view.coffee index 9b2de1b198..9399d95398 100644 --- a/common/static/coffee/src/discussion/views/discussion_content_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_content_view.coffee @@ -1,11 +1,6 @@ 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) diff --git a/common/static/coffee/src/discussion/views/discussion_thread_list_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_list_view.coffee index 9aa4ba869d..8364963218 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_list_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_list_view.coffee @@ -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") diff --git a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee index 49936c46e8..56525af347 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee @@ -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 diff --git a/common/static/coffee/src/discussion/views/discussion_thread_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_view.coffee index c3a793b478..cb549f1088 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_view.coffee @@ -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() diff --git a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee index 18d405fdb4..84e7357e1f 100644 --- a/common/static/coffee/src/discussion/views/response_comment_show_view.coffee +++ b/common/static/coffee/src/discussion/views/response_comment_show_view.coffee @@ -1,14 +1,7 @@ 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('staff') else if DiscussionUtil.isTA(@model.get("user_id")) @$el.find("a.profile-link").after('Community  TA') - - - 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() - - diff --git a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee index 0e42b79b9a..1f305ddf34 100644 --- a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee +++ b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee @@ -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() diff --git a/common/static/coffee/src/discussion/views/thread_response_view.coffee b/common/static/coffee/src/discussion/views/thread_response_view.coffee index 46a96a55ec..9b6800cdde 100644 --- a/common/static/coffee/src/discussion/views/thread_response_view.coffee +++ b/common/static/coffee/src/discussion/views/thread_response_view.coffee @@ -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 diff --git a/distribute-0.6.32.tar.gz b/distribute-0.6.32.tar.gz new file mode 100644 index 0000000000..2438db60fa Binary files /dev/null and b/distribute-0.6.32.tar.gz differ diff --git a/distribute-0.6.34.tar.gz b/distribute-0.6.34.tar.gz new file mode 100644 index 0000000000..4e91b3af62 Binary files /dev/null and b/distribute-0.6.34.tar.gz differ diff --git a/jenkins/test.sh b/jenkins/test.sh index e1d44bf6b5..53643b8440 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -40,6 +40,8 @@ yes w | pip install -q -r requirements.txt bundle install +npm install + rake clobber rake pep8 > pep8.log || cat pep8.log rake pylint > pylint.log || cat pylint.log diff --git a/lms/djangoapps/django_comment_client/base/urls.py b/lms/djangoapps/django_comment_client/base/urls.py index 18efbec502..92826a18ae 100644 --- a/lms/djangoapps/django_comment_client/base/urls.py +++ b/lms/djangoapps/django_comment_client/base/urls.py @@ -11,8 +11,6 @@ urlpatterns = patterns('django_comment_client.base.views', url(r'threads/(?P[\w\-]+)/delete', 'delete_thread', name='delete_thread'), url(r'threads/(?P[\w\-]+)/upvote$', 'vote_for_thread', {'value': 'up'}, name='upvote_thread'), url(r'threads/(?P[\w\-]+)/downvote$', 'vote_for_thread', {'value': 'down'}, name='downvote_thread'), - url(r'threads/(?P[\w\-]+)/flagAbuse$', 'flag_abuse_for_thread', name='flag_abuse_for_thread'), - url(r'threads/(?P[\w\-]+)/unFlagAbuse$', 'un_flag_abuse_for_thread', name='un_flag_abuse_for_thread'), url(r'threads/(?P[\w\-]+)/unvote$', 'undo_vote_for_thread', name='undo_vote_for_thread'), url(r'threads/(?P[\w\-]+)/pin$', 'pin_thread', name='pin_thread'), url(r'threads/(?P[\w\-]+)/unpin$', 'un_pin_thread', name='un_pin_thread'), @@ -27,8 +25,7 @@ urlpatterns = patterns('django_comment_client.base.views', url(r'comments/(?P[\w\-]+)/upvote$', 'vote_for_comment', {'value': 'up'}, name='upvote_comment'), url(r'comments/(?P[\w\-]+)/downvote$', 'vote_for_comment', {'value': 'down'}, name='downvote_comment'), url(r'comments/(?P[\w\-]+)/unvote$', 'undo_vote_for_comment', name='undo_vote_for_comment'), - url(r'comments/(?P[\w\-]+)/flagAbuse$', 'flag_abuse_for_comment', name='flag_abuse_for_comment'), - url(r'comments/(?P[\w\-]+)/unFlagAbuse$', 'un_flag_abuse_for_comment', name='un_flag_abuse_for_comment'), + url(r'^(?P[\w\-.]+)/threads/create$', 'create_thread', name='create_thread'), # TODO should we search within the board? url(r'^(?P[\w\-.]+)/threads/search_similar$', 'search_similar_threads', name='search_similar_threads'), diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index d2fa25e979..69609dcf01 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -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) @@ -284,50 +284,6 @@ def vote_for_thread(request, course_id, thread_id, value): return JsonResponse(utils.safe_content(thread.to_dict())) -@require_POST -@login_required -@permitted -def flag_abuse_for_thread(request, course_id, thread_id): - user = cc.User.from_django_user(request.user) - thread = cc.Thread.find(thread_id) - thread.flagAbuse(user, thread) - return JsonResponse(utils.safe_content(thread.to_dict())) - - -@require_POST -@login_required -@permitted -def un_flag_abuse_for_thread(request, course_id, thread_id): - user = cc.User.from_django_user(request.user) - course = get_course_by_id(course_id) - thread = cc.Thread.find(thread_id) - removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff') - thread.unFlagAbuse(user, thread, removeAll) - return JsonResponse(utils.safe_content(thread.to_dict())) - - -@require_POST -@login_required -@permitted -def flag_abuse_for_comment(request, course_id, comment_id): - user = cc.User.from_django_user(request.user) - comment = cc.Comment.find(comment_id) - comment.flagAbuse(user, comment) - return JsonResponse(utils.safe_content(comment.to_dict())) - - -@require_POST -@login_required -@permitted -def un_flag_abuse_for_comment(request, course_id, comment_id): - user = cc.User.from_django_user(request.user) - course = get_course_by_id(course_id) - removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff') - comment = cc.Comment.find(comment_id) - comment.unFlagAbuse(user, comment, removeAll) - return JsonResponse(utils.safe_content(comment.to_dict())) - - @require_POST @login_required @permitted @@ -337,21 +293,19 @@ def undo_vote_for_thread(request, course_id, thread_id): user.unvote(thread) return JsonResponse(utils.safe_content(thread.to_dict())) - @require_POST @login_required @permitted def pin_thread(request, course_id, thread_id): user = cc.User.from_django_user(request.user) thread = cc.Thread.find(thread_id) - thread.pin(user, thread_id) + thread.pin(user,thread_id) return JsonResponse(utils.safe_content(thread.to_dict())) - def un_pin_thread(request, course_id, thread_id): user = cc.User.from_django_user(request.user) thread = cc.Thread.find(thread_id) - thread.un_pin(user, thread_id) + thread.un_pin(user,thread_id) return JsonResponse(utils.safe_content(thread.to_dict())) @@ -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: diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index a94b9a07ad..6498ea8370 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -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): diff --git a/lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py b/lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py index e84771d615..5e7e268270 100644 --- a/lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py +++ b/lms/djangoapps/django_comment_client/management/commands/reload_forum_users.py @@ -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 diff --git a/lms/djangoapps/django_comment_client/permissions.py b/lms/djangoapps/django_comment_client/permissions.py index cc3ead53e7..7d21cc9783 100644 --- a/lms/djangoapps/django_comment_client/permissions.py +++ b/lms/djangoapps/django_comment_client/permissions.py @@ -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'], diff --git a/lms/djangoapps/django_comment_client/tests/test_mustache_helpers.py b/lms/djangoapps/django_comment_client/tests/test_mustache_helpers.py index d5a403ecb8..7db3ba6e86 100644 --- a/lms/djangoapps/django_comment_client/tests/test_mustache_helpers.py +++ b/lms/djangoapps/django_comment_client/tests/test_mustache_helpers.py @@ -39,3 +39,4 @@ class CloseThreadTextTest(TestCase): self.assertEqual(mustache_helpers.close_thread_text(self.contentOpen), 'Close thread') ######################################################################################### + diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index c79cc4cb89..9bfb9a9d0d 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -1,4 +1,3 @@ -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): diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 8363f744a0..0b03089774 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -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 ######################## diff --git a/lms/lib/comment_client/comment.py b/lms/lib/comment_client/comment.py index fb5a4ad0c3..2f93aff6b3 100644 --- a/lms/lib/comment_client/comment.py +++ b/lms/lib/comment_client/comment.py @@ -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) diff --git a/lms/lib/comment_client/comment_client.py b/lms/lib/comment_client/comment_client.py index 9b1a0baee2..862483a75b 100644 --- a/lms/lib/comment_client/comment_client.py +++ b/lms/lib/comment_client/comment_client.py @@ -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) diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 0b0be576b8..8911d5a2c6 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -1,4 +1,5 @@ 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 diff --git a/lms/static/coffee/.gitignore b/lms/static/coffee/.gitignore index bb90193362..a6c7c2852d 100644 --- a/lms/static/coffee/.gitignore +++ b/lms/static/coffee/.gitignore @@ -1,2 +1 @@ *.js -module diff --git a/lms/static/images/flagged.png b/lms/static/images/flagged.png deleted file mode 100644 index c3de857733..0000000000 Binary files a/lms/static/images/flagged.png and /dev/null differ diff --git a/lms/static/images/notflagged.png b/lms/static/images/notflagged.png deleted file mode 100644 index 42bc0b3aff..0000000000 Binary files a/lms/static/images/notflagged.png and /dev/null differ diff --git a/lms/static/images/resolvedflag.png b/lms/static/images/resolvedflag.png deleted file mode 100644 index 8e318f786c..0000000000 Binary files a/lms/static/images/resolvedflag.png and /dev/null differ diff --git a/lms/static/sass/.gitignore b/lms/static/sass/.gitignore index c8578e8cd3..b3a5267117 100644 --- a/lms/static/sass/.gitignore +++ b/lms/static/sass/.gitignore @@ -1,2 +1 @@ *.css -module diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index c03785859e..9583a8d30f 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -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 diff --git a/lms/templates/discussion/_filter_dropdown.html b/lms/templates/discussion/_filter_dropdown.html index dd5b94f910..fef4abb11f 100644 --- a/lms/templates/discussion/_filter_dropdown.html +++ b/lms/templates/discussion/_filter_dropdown.html @@ -33,14 +33,6 @@ Show All Discussions - %if flag_moderator: -
  • - - Show Flagged Discussions - -
  • - - %endif
  • Following diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index 110e6ffc19..24e3b467be 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -3,7 +3,6 @@