def test_is_commentable_cohorted(self): course = modulestore().get_course(self.toy_course_key) self.assertFalse(cohorts.is_course_cohorted(course.id)) def to_id(name): """Helper for topic_name_to_id that uses course.""" return topic_name_to_id(course, name) # no topics self.assertFalse( utils.is_commentable_cohorted(course.id, to_id("General")), "Course doesn't even have a 'General' topic" ) # not cohorted config_course_cohorts(course, is_cohorted=False, discussion_topics=["General", "Feedback"]) self.assertFalse( utils.is_commentable_cohorted(course.id, to_id("General")), "Course isn't cohorted" ) # cohorted, but top level topics aren't config_course_cohorts(course, is_cohorted=True, discussion_topics=["General", "Feedback"]) self.assertTrue(cohorts.is_course_cohorted(course.id)) self.assertFalse( utils.is_commentable_cohorted(course.id, to_id("General")), "Course is cohorted, but 'General' isn't." ) # cohorted, including "Feedback" top-level topics aren't config_course_cohorts( course, is_cohorted=True, discussion_topics=["General", "Feedback"], cohorted_discussions=["Feedback"] ) self.assertTrue(cohorts.is_course_cohorted(course.id)) self.assertFalse( utils.is_commentable_cohorted(course.id, to_id("General")), "Course is cohorted, but 'General' isn't." ) self.assertTrue( utils.is_commentable_cohorted(course.id, to_id("Feedback")), "Feedback was listed as cohorted. Should be." )
def move_to_verified_cohort(sender, instance, **kwargs): # pylint: disable=unused-argument """ If the learner has changed modes, update assigned cohort iff the course is using the Automatic Verified Track Cohorting MVP feature. """ course_key = instance.course_id verified_cohort_enabled = VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled(course_key) verified_cohort_name = VerifiedTrackCohortedCourse.verified_cohort_name_for_course(course_key) if verified_cohort_enabled and (instance.mode != instance._old_mode): # pylint: disable=protected-access if not is_course_cohorted(course_key): log.error("Automatic verified cohorting enabled for course '%s', but course is not cohorted", course_key) else: existing_cohorts = get_course_cohorts(get_course_by_id(course_key), CourseCohort.MANUAL) if any(cohort.name == verified_cohort_name for cohort in existing_cohorts): args = { 'course_id': unicode(course_key), 'user_id': instance.user.id, 'verified_cohort_name': verified_cohort_name } # Do the update with a 3-second delay in hopes that the CourseEnrollment transaction has been # completed before the celery task runs. We want a reasonably short delay in case the learner # immediately goes to the courseware. sync_cohort_with_mode.apply_async(kwargs=args, countdown=3) # In case the transaction actually was not committed before the celery task runs, # run it again after 5 minutes. If the first completed successfully, this task will be a no-op. sync_cohort_with_mode.apply_async(kwargs=args, countdown=300) else: log.error( "Automatic verified cohorting enabled for course '%s', but cohort named '%s' does not exist.", course_key, verified_cohort_name, )
def test_is_commentable_cohorted_team(self): course = modulestore().get_course(self.toy_course_key) self.assertFalse(cohorts.is_course_cohorted(course.id)) config_course_cohorts(course, is_cohorted=True) team = CourseTeamFactory(course_id=course.id) # Verify that team discussions are not cohorted, but other discussions are self.assertFalse(utils.is_commentable_cohorted(course.id, team.discussion_topic_id)) self.assertTrue(utils.is_commentable_cohorted(course.id, "random"))
def make_course_settings(course, user): """ Generate a JSON-serializable model for course settings, which will be used to initialize a DiscussionCourseSettings object on the client. """ return { 'is_cohorted': is_course_cohorted(course.id), 'allow_anonymous': course.allow_anonymous, 'allow_anonymous_to_peers': course.allow_anonymous_to_peers, 'cohorts': [{"id": str(g.id), "name": g.name} for g in get_course_cohorts(course)], 'category_map': utils.get_discussion_category_map(course, user) }
def _get_course_division_scheme(course_discussion_settings): division_scheme = course_discussion_settings.division_scheme if ( division_scheme == CourseDiscussionSettings.COHORT and not is_course_cohorted(course_discussion_settings.course_id) ): division_scheme = CourseDiscussionSettings.NONE elif ( division_scheme == CourseDiscussionSettings.ENROLLMENT_TRACK and enrollment_track_group_count(course_discussion_settings.course_id) <= 1 ): division_scheme = CourseDiscussionSettings.NONE return division_scheme
def _section_send_email(course, access): """ Provide data for the corresponding bulk email section """ course_key = course.id # Monkey-patch applicable_aside_types to return no asides for the duration of this render with patch.object(course.runtime, 'applicable_aside_types', null_applicable_aside_types): # This HtmlDescriptor is only being used to generate a nice text editor. html_module = HtmlDescriptor( course.system, DictFieldData({'data': ''}), ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake')) ) fragment = course.system.render(html_module, 'studio_view') fragment = wrap_xblock( 'LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": unicode(course_key)}, usage_id_serializer=lambda usage_id: quote_slashes(unicode(usage_id)), # Generate a new request_token here at random, because this module isn't connected to any other # xblock rendering. request_token=uuid.uuid1().get_hex() ) cohorts = [] if is_course_cohorted(course_key): cohorts = get_course_cohorts(course) course_modes = [] if not VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled(course_key): course_modes = CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False) email_editor = fragment.content section_data = { 'section_key': 'send_email', 'section_display_name': _('Email'), 'access': access, 'send_email': reverse('send_email', kwargs={'course_id': unicode(course_key)}), 'editor': email_editor, 'cohorts': cohorts, 'course_modes': course_modes, 'default_cohort_name': DEFAULT_COHORT_NAME, 'list_instructor_tasks_url': reverse( 'list_instructor_tasks', kwargs={'course_id': unicode(course_key)} ), 'email_background_tasks_url': reverse( 'list_background_email_tasks', kwargs={'course_id': unicode(course_key)} ), 'email_content_history_url': reverse( 'list_email_content', kwargs={'course_id': unicode(course_key)} ), } from openedx.stanford.lms.djangoapps.instructor.views.instructor_dashboard import send_email_section_data section_data.update(send_email_section_data()) return section_data
def available_division_schemes(course_key): """ Returns a list of possible discussion division schemes for this course. This takes into account if cohorts are enabled and if there are multiple enrollment tracks. If no schemes are available, returns an empty list. Args: course_key: CourseKey Returns: list of possible division schemes (for example, CourseDiscussionSettings.COHORT) """ available_schemes = [] if is_course_cohorted(course_key): available_schemes.append(CourseDiscussionSettings.COHORT) if enrollment_track_group_count(course_key) > 1: available_schemes.append(CourseDiscussionSettings.ENROLLMENT_TRACK) return available_schemes
def _set_group_names(course_id, threads): """ Adds group name if the thread has a group id""" for thread in threads: if thread.get('group_id') and is_course_cohorted(course_id): thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name thread['group_string'] = "This post visible only to Group %s." % (thread['group_name']) else: thread['group_name'] = "" thread['group_string'] = "This post visible to everyone." #patch for backward compatibility to comments service if 'pinned' not in thread: thread['pinned'] = False return threads
def _section_discussions_management(course, access): """ Provide data for the corresponding discussion management section """ course_key = course.id enrollment_track_schemes = available_division_schemes(course_key) section_data = { 'section_key': 'discussions_management', 'section_display_name': _('Discussions'), 'is_hidden': (not is_course_cohorted(course_key) and CourseDiscussionSettings.ENROLLMENT_TRACK not in enrollment_track_schemes), 'discussion_topics_url': reverse('discussion_topics', kwargs={'course_key_string': unicode(course_key)}), 'course_discussion_settings': reverse( 'course_discussions_settings', kwargs={'course_key_string': unicode(course_key)} ), } return section_data
def move_to_verified_cohort(sender, instance, **kwargs): # pylint: disable=unused-argument """ If the learner has changed modes, update assigned cohort iff the course is using the Automatic Verified Track Cohorting MVP feature. """ course_key = instance.course_id verified_cohort_enabled = VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled(course_key) verified_cohort_name = VerifiedTrackCohortedCourse.verified_cohort_name_for_course(course_key) if verified_cohort_enabled and (instance.mode != instance._old_mode): # pylint: disable=protected-access if not is_course_cohorted(course_key): log.error("Automatic verified cohorting enabled for course '%s', but course is not cohorted.", course_key) else: course = get_course_by_id(course_key) existing_manual_cohorts = get_course_cohorts(course, CourseCohort.MANUAL) if any(cohort.name == verified_cohort_name for cohort in existing_manual_cohorts): # Get a random cohort to use as the default cohort (for audit learners). # Note that calling this method will create a "Default Group" random cohort if no random # cohort yet exist. random_cohort = get_random_cohort(course_key) args = { 'course_id': unicode(course_key), 'user_id': instance.user.id, 'verified_cohort_name': verified_cohort_name, 'default_cohort_name': random_cohort.name } log.info( "Queuing automatic cohorting for user '%s' in course '%s' " "due to change in enrollment mode from '%s' to '%s'.", instance.user.id, course_key, instance._old_mode, instance.mode # pylint: disable=protected-access ) # Do the update with a 3-second delay in hopes that the CourseEnrollment transaction has been # completed before the celery task runs. We want a reasonably short delay in case the learner # immediately goes to the courseware. sync_cohort_with_mode.apply_async(kwargs=args, countdown=3) # In case the transaction actually was not committed before the celery task runs, # run it again after 5 minutes. If the first completed successfully, this task will be a no-op. sync_cohort_with_mode.apply_async(kwargs=args, countdown=300) else: log.error( "Automatic verified cohorting enabled for course '%s', " "but verified cohort named '%s' does not exist.", course_key, verified_cohort_name, )
def test_is_commentable_cohorted_inline_discussion(self): course = modulestore().get_course(self.toy_course_key) self.assertFalse(cohorts.is_course_cohorted(course.id)) def to_id(name): # pylint: disable=missing-docstring return topic_name_to_id(course, name) config_course_cohorts( course, is_cohorted=True, discussion_topics=["General", "Feedback"], cohorted_discussions=["Feedback", "random_inline"], ) self.assertTrue( utils.is_commentable_cohorted(course.id, to_id("random")), "By default, Non-top-level discussion is always cohorted in cohorted courses.", ) # if always_cohort_inline_discussions is set to False, non-top-level discussion are always # non cohorted unless they are explicitly set in cohorted_discussions config_course_cohorts( course, is_cohorted=True, discussion_topics=["General", "Feedback"], cohorted_discussions=["Feedback", "random_inline"], always_cohort_inline_discussions=False, ) self.assertFalse( utils.is_commentable_cohorted(course.id, to_id("random")), "Non-top-level discussion is not cohorted if always_cohort_inline_discussions is False.", ) self.assertTrue( utils.is_commentable_cohorted(course.id, to_id("random_inline")), "If always_cohort_inline_discussions set to False, Non-top-level discussion is " "cohorted if explicitly set in cohorted_discussions.", ) self.assertTrue( utils.is_commentable_cohorted(course.id, to_id("Feedback")), "If always_cohort_inline_discussions set to False, top-level discussion are not affected.", )
def get_course_info(self, user): cohort_id_map = { cohort.course_id: cohort.id for cohort in user.course_groups.all() } see_all_cohorts_set = { role.course_id for role in user.roles.all() for perm in role.permissions.all() if perm.name == "see_all_cohorts" } ret = {} for enrollment in user.courseenrollment_set.all(): if enrollment.is_active: try: ret[unicode(enrollment.course_id)] = { "cohort_id": cohort_id_map.get(enrollment.course_id), "see_all_cohorts": ( enrollment.course_id in see_all_cohorts_set or not is_course_cohorted(enrollment.course_id) ), } except Http404: # is_course_cohorted raises this if course does not exist pass return ret
def single_thread(request, course_key, discussion_id, thread_id): """ Renders a response to display a single discussion thread. This could either be a page refresh after navigating to a single thread, a direct link to a single thread, or an AJAX call from the discussions UI loading the responses/comments for a single thread. Depending on the HTTP headers, we'll adjust our response accordingly. """ nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course_settings = make_course_settings(course, request.user) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() is_moderator = has_permission(request.user, "see_all_cohorts", course_key) is_staff = has_permission(request.user, 'openclose_thread', course.id) try: thread = cc.Thread.find(thread_id).retrieve( with_responses=request.is_ajax(), recursive=request.is_ajax(), user_id=request.user.id, response_skip=request.GET.get("resp_skip"), response_limit=request.GET.get("resp_limit") ) except cc.utils.CommentClientRequestError as error: if error.status_code == 404: raise Http404 raise # Verify that the student has access to this thread if belongs to a course discussion module thread_context = getattr(thread, "context", "course") if thread_context == "course" and not utils.discussion_category_id_access(course, request.user, discussion_id): raise Http404 # verify that the thread belongs to the requesting student's cohort if is_commentable_cohorted(course_key, discussion_id) and not is_moderator: user_group_id = get_cohort_id(request.user, course_key) if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id: raise Http404 if request.is_ajax(): with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): annotated_content_info = utils.get_annotated_content_infos( course_key, thread, request.user, user_info=user_info ) content = utils.prepare_content(thread.to_dict(), course_key, is_staff) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context([content], course, request.user) return utils.JsonResponse({ 'content': content, 'annotated_content_info': annotated_content_info, }) else: # Since we're in page render mode, and the discussions UI will request the thread list itself, # we need only return the thread information for this one. threads = [thread.to_dict()] with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) for thread in threads: # patch for backward compatibility with comments service if "pinned" not in thread: thread["pinned"] = False threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads] with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): user_cohort = get_cohort_id(request.user, course_key) context = { 'discussion_id': discussion_id, 'csrf': csrf(request)['csrf_token'], 'init': '', # TODO: What is this? 'user_info': user_info, 'can_create_comment': has_permission(request.user, "create_comment", course.id), 'can_create_subcomment': has_permission(request.user, "create_sub_comment", course.id), 'can_create_thread': has_permission(request.user, "create_thread", course.id), 'annotated_content_info': annotated_content_info, 'course': course, #'recent_active_threads': recent_active_threads, 'course_id': course.id.to_deprecated_string(), # TODO: Why pass both course and course.id to template? 'thread_id': thread_id, 'threads': threads, 'roles': utils.get_role_ids(course_key), 'is_moderator': is_moderator, 'thread_pages': 1, 'is_course_cohorted': is_course_cohorted(course_key), 'flag_moderator': bool( has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course) ), 'cohorts': course_settings["cohorts"], 'user_cohort': user_cohort, 'sort_preference': cc_user.default_sort_key, 'category_map': course_settings["category_map"], 'course_settings': course_settings, 'disable_courseware_js': True, 'uses_pattern_library': True, } return render_to_response('discussion/discussion_board.html', context)
def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): """ For a given `course_id`, generate a grades CSV file for all students that are enrolled, and store using a `ReportStore`. Once created, the files can be accessed by instantiating another `ReportStore` (via `ReportStore.from_config()`) and calling `link_for()` on it. Writes are buffered, so we'll never write part of a CSV file to S3 -- i.e. any files that are visible in ReportStore will be complete ones. As we start to add more CSV downloads, it will probably be worthwhile to make a more general CSVDoc class instead of building out the rows like we do here. """ start_time = time() start_date = datetime.now(UTC) status_interval = 100 enrolled_students = CourseEnrollment.users_enrolled_in(course_id) task_progress = TaskProgress(action_name, enrolled_students.count(), start_time) fmt = u'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}' task_info_string = fmt.format( task_id=_xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None, entry_id=_entry_id, course_id=course_id, task_input=_task_input ) TASK_LOG.info(u'%s, Task type: %s, Starting task execution', task_info_string, action_name) course = get_course_by_id(course_id) course_is_cohorted = is_course_cohorted(course.id) cohorts_header = ['Cohort Name'] if course_is_cohorted else [] experiment_partitions = get_split_user_partitions(course.user_partitions) group_configs_header = [u'Experiment Group ({})'.format(partition.name) for partition in experiment_partitions] # Loop over all our students and build our CSV lists in memory header = None rows = [] err_rows = [["id", "username", "error_msg"]] current_step = {'step': 'Calculating Grades'} total_enrolled_students = enrolled_students.count() student_counter = 0 TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Starting grade calculation for total students: %s', task_info_string, action_name, current_step, total_enrolled_students ) for student, gradeset, err_msg in iterate_grades_for(course_id, enrolled_students): # Periodically update task status (this is a cache write) if task_progress.attempted % status_interval == 0: task_progress.update_task_state(extra_meta=current_step) task_progress.attempted += 1 # Now add a log entry after certain intervals to get a hint that task is in progress student_counter += 1 if student_counter % 1000 == 0: TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Grade calculation in-progress for students: %s/%s', task_info_string, action_name, current_step, student_counter, total_enrolled_students ) if gradeset: # We were able to successfully grade this student for this course. task_progress.succeeded += 1 if not header: header = [section['label'] for section in gradeset[u'section_breakdown']] rows.append( ["id", "email", "username", "grade"] + header + cohorts_header + group_configs_header ) percents = { section['label']: section.get('percent', 0.0) for section in gradeset[u'section_breakdown'] if 'label' in section } cohorts_group_name = [] if course_is_cohorted: group = get_cohort(student, course_id, assign=False) cohorts_group_name.append(group.name if group else '') group_configs_group_names = [] for partition in experiment_partitions: group = LmsPartitionService(student, course_id).get_group(partition, assign=False) group_configs_group_names.append(group.name if group else '') # Not everybody has the same gradable items. If the item is not # found in the user's gradeset, just assume it's a 0. The aggregated # grades for their sections and overall course will be calculated # without regard for the item they didn't have access to, so it's # possible for a student to have a 0.0 show up in their row but # still have 100% for the course. row_percents = [percents.get(label, 0.0) for label in header] rows.append( [student.id, student.email, student.username, gradeset['percent']] + row_percents + cohorts_group_name + group_configs_group_names ) else: # An empty gradeset means we failed to grade a student. task_progress.failed += 1 err_rows.append([student.id, student.username, err_msg]) TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Grade calculation completed for students: %s/%s', task_info_string, action_name, current_step, student_counter, total_enrolled_students ) # By this point, we've got the rows we're going to stuff into our CSV files. current_step = {'step': 'Uploading CSVs'} task_progress.update_task_state(extra_meta=current_step) TASK_LOG.info(u'%s, Task type: %s, Current step: %s', task_info_string, action_name, current_step) # Perform the actual upload upload_csv_to_report_store(rows, 'grade_report', course_id, start_date) # If there are any error rows (don't count the header), write them out as well if len(err_rows) > 1: upload_csv_to_report_store(err_rows, 'grade_report_err', course_id, start_date) # One last update before we close out... TASK_LOG.info(u'%s, Task type: %s, Finalizing grade task', task_info_string, action_name) return task_progress.update_task_state(extra_meta=current_step)
def upload_user_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): # pylint: disable=too-many-statements """ For a given `course_id`, for given usernames generates a grades CSV file, and store using a `ReportStore`. Once created, the files can be accessed by instantiating another `ReportStore` (via `ReportStore.from_config()`) and calling `link_for()` on it. Unenrolled users and unknown usernames are stored in *_err_*.csv This task is very close to the .upload_grades_csv from instructor_tasks.task_helper The difference is that we filter enrolled students against requested usernames and we push info about this into PLP """ start_time = time() start_date = datetime.now(UTC) status_interval = 100 fmt = u'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}' task_info_string = fmt.format( task_id=_xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None, entry_id=_entry_id, course_id=course_id, task_input=_task_input ) TASK_LOG.info(u'%s, Task type: %s, Starting task execution', task_info_string, action_name) extended_kwargs_id = _task_input.get("extended_kwargs_id") extended_kwargs = InstructorTaskExtendedKwargs.get_kwargs_for_id(extended_kwargs_id) usernames = extended_kwargs.get("usernames", None) err_rows = [["id", "username", "error_msg"]] if usernames is None: message = "Error occured during edx task execution: no usersnames in InstructorTaskExtendedKwargs." TASK_LOG.error(u'%s, Task type: %s, ' + message, task_info_string) err_rows.append(["-1", "__", message]) usernames = [] enrolled_students = CourseEnrollment.objects.users_enrolled_in(course_id) enrolled_students = enrolled_students.filter(username__in=usernames) total_enrolled_students = enrolled_students.count() requester_id = _task_input.get("requester_id") task_progress = TaskProgress(action_name, total_enrolled_students, start_time) course = get_course_by_id(course_id) course_is_cohorted = is_course_cohorted(course.id) teams_enabled = course.teams_enabled cohorts_header = ['Cohort Name'] if course_is_cohorted else [] teams_header = ['Team Name'] if teams_enabled else [] experiment_partitions = get_split_user_partitions(course.user_partitions) group_configs_header = [u'Experiment Group ({})'.format(partition.name) for partition in experiment_partitions] certificate_info_header = ['Certificate Eligible', 'Certificate Delivered', 'Certificate Type'] certificate_whitelist = CertificateWhitelist.objects.filter(course_id=course_id, whitelist=True) whitelisted_user_ids = [entry.user_id for entry in certificate_whitelist] # Loop over all our students and build our CSV lists in memory rows = [] current_step = {'step': 'Calculating Grades'} TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Starting grade calculation for total students: %s', task_info_string, action_name, current_step, total_enrolled_students, ) found_students = User.objects.filter(username__in=usernames) # Check invalid usernames if len(found_students)!= len(usernames): found_students_usernames = [x.username for x in found_students] for u in usernames: if u not in found_students_usernames: err_rows.append([-1, u, "invalid_username"]) # Check not enrolled requested students if found_students != enrolled_students: diff = found_students.exclude(id__in=enrolled_students) for u in diff: if u in diff: err_rows.append([u.id, u.username, "enrollment_for_username_not_found"]) total_enrolled_students = enrolled_students.count() student_counter = 0 TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Starting grade calculation for total students: %s', task_info_string, action_name, current_step, total_enrolled_students ) graded_assignments = course.grading.graded_assignments(course_id) grade_header = course.grading.grade_header(graded_assignments) rows.append( ["Student ID", "Email", "Username", "Last Name", "First Name", "Second Name", "Grade", "Grade Percent"] + grade_header + cohorts_header + group_configs_header + teams_header + ['Enrollment Track', 'Verification Status'] + certificate_info_header ) for student, course_grade, err_msg in CourseGradeFactory().iter(course, enrolled_students): # Periodically update task status (this is a cache write) if task_progress.attempted % status_interval == 0: task_progress.update_task_state(extra_meta=current_step) task_progress.attempted += 1 # Now add a log entry after each student is graded to get a sense # of the task's progress student_counter += 1 TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Grade calculation in-progress for students: %s/%s', task_info_string, action_name, current_step, student_counter, total_enrolled_students ) if not course_grade: # An empty course_grade means we failed to grade a student. task_progress.failed += 1 err_rows.append([student.id, student.username, err_msg]) continue # We were able to successfully grade this student for this course. task_progress.succeeded += 1 cohorts_group_name = [] if course_is_cohorted: group = get_cohort(student, course_id, assign=False) cohorts_group_name.append(group.name if group else '') group_configs_group_names = [] for partition in experiment_partitions: group = PartitionService(course_id).get_group(student, partition, assign=False) group_configs_group_names.append(group.name if group else '') team_name = [] if teams_enabled: try: membership = CourseTeamMembership.objects.get(user=student, team__course_id=course_id) team_name.append(membership.team.name) except CourseTeamMembership.DoesNotExist: team_name.append('') enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)[0] verification_status = SoftwareSecurePhotoVerification.verification_status_for_user( student, course_id, enrollment_mode ) certificate_info = certificate_info_for_user( student, course_id, course_grade.letter_grade, student.id in whitelisted_user_ids ) second_name = '' try: up = UserProfile.objects.get(user=student) if up.goals: second_name = json.loads(up.goals).get('second_name', '') except ValueError: pass if certificate_info[0] == 'Y': TASK_LOG.info( u'Student is marked eligible_for_certificate' u'(user=%s, course_id=%s, grade_percent=%s gradecutoffs=%s, allow_certificate=%s, is_whitelisted=%s)', student, course_id, course_grade.percent, course.grade_cutoffs, student.profile.allow_certificate, student.id in whitelisted_user_ids ) grade_results = course.grading.grade_results(graded_assignments, course_grade) grade_results = list(chain.from_iterable(grade_results)) rows.append( [student.id, student.email, student.username, student.last_name, student.first_name, second_name, course_grade.percent, course_grade.percent*100] + grade_results + cohorts_group_name + group_configs_group_names + team_name + [enrollment_mode] + [verification_status] + certificate_info ) TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Grade calculation completed for students: %s/%s', task_info_string, action_name, current_step, student_counter, total_enrolled_students ) # By this point, we've got the rows we're going to stuff into our CSV files. current_step = {'step': 'Uploading CSVs'} task_progress.update_task_state(extra_meta=current_step) TASK_LOG.info(u'%s, Task type: %s, Current step: %s', task_info_string, action_name, current_step) # Perform the actual upload custom_grades_download = get_custom_grade_config() report_hash_unique_hash = hex(random.getrandbits(32))[2:] report_name = 'plp_grade_users_report_{}_id_{}'.format(report_hash_unique_hash, requester_id) err_report_name = 'plp_grade_users_report_err_{}_id_{}'.format(report_hash_unique_hash, requester_id) upload_csv_to_report_store(rows, report_name, course_id, start_date, config_name=custom_grades_download) # If there are any error rows (don't count the header), write them out as well has_errors = len(err_rows) > 1 if has_errors: upload_csv_to_report_store(err_rows, err_report_name, course_id, start_date, config_name=custom_grades_download) callback_url = _task_input.get("callback_url", None) if callback_url: report_store = ReportStore.from_config(config_name=custom_grades_download) files_urls_pairs = report_store.links_for(course_id) find_by_name = lambda name: [url for filename, url in files_urls_pairs if name in filename][0] try: csv_url = find_by_name(report_name) csv_err_url = find_by_name(err_report_name) if has_errors else None PlpApiClient().push_grade_api_result(callback_url, csv_url, csv_err_url) except Exception as e: TASK_LOG.error("Failed push to PLP:{}".format(str(e))) # One last update before we close out... TASK_LOG.info(u'%s, Task type: %s, Finalizing grade task', task_info_string, action_name) return task_progress.update_task_state(extra_meta=current_step)
def get_threads(request, course, user_info, discussion_id=None, per_page=THREADS_PER_PAGE): """ This may raise an appropriate subclass of cc.utils.CommentClientError if something goes wrong, or ValueError if the group_id is invalid. Arguments: request (WSGIRequest): The user request. course (CourseDescriptorWithMixins): The course object. user_info (dict): The comment client User object as a dict. discussion_id (unicode): Optional discussion id/commentable id for context. per_page (int): Optional number of threads per page. Returns: (tuple of list, dict): A tuple of the list of threads and a dict of the query parameters used for the search. """ default_query_params = { 'page': 1, 'per_page': per_page, 'sort_key': 'activity', 'text': '', 'course_id': unicode(course.id), 'user_id': request.user.id, 'context': ThreadContext.COURSE, 'group_id': get_group_id_for_comments_service(request, course.id, discussion_id), # may raise ValueError } # If provided with a discussion id, filter by discussion id in the # comments_service. if discussion_id is not None: default_query_params['commentable_id'] = discussion_id # Use the discussion id/commentable id to determine the context we are going to pass through to the backend. if get_team(discussion_id) is not None: default_query_params['context'] = ThreadContext.STANDALONE if not request.GET.get('sort_key'): # If the user did not select a sort key, use their last used sort key default_query_params['sort_key'] = user_info.get('default_sort_key') or default_query_params['sort_key'] elif request.GET.get('sort_key') != user_info.get('default_sort_key'): # If the user clicked a sort key, update their default sort key cc_user = cc.User.from_django_user(request.user) cc_user.default_sort_key = request.GET.get('sort_key') cc_user.save() # there are 2 dimensions to consider when executing a search with respect to group id # is user a moderator # did the user request a group # if the user requested a group explicitly, give them that group, otherwise, if mod, show all, else if student, use cohort if discussion_id: is_cohorted = is_commentable_divided(course.id, discussion_id) else: is_cohorted = is_course_cohorted(course.id) if has_permission(request.user, "see_all_cohorts", course.id): group_id = request.GET.get('group_id') if group_id in ("all", "None"): group_id = None else: group_id = get_cohort_id(request.user, course.id) if not group_id: default_query_params['exclude_groups'] = True if group_id: group_id = int(group_id) try: CourseUserGroup.objects.get(course_id=course.id, id=group_id) except CourseUserGroup.DoesNotExist: if not is_cohorted: group_id = None else: raise ValueError("Invalid Group ID") default_query_params["group_id"] = group_id #so by default, a moderator sees all items, and a student sees his cohort query_params = merge_dict( default_query_params, strip_none( extract( request.GET, [ 'page', 'sort_key', 'text', 'commentable_ids', 'flagged', 'unread', 'unanswered', ] ) ) ) if not is_cohorted: query_params.pop('group_id', None) paginated_results = cc.Thread.search(query_params) threads = paginated_results.collection # If not provided with a discussion id, filter threads by commentable ids # which are accessible to the current user. if discussion_id is None: discussion_category_ids = set(utils.get_discussion_categories_ids(course, request.user)) threads = [ thread for thread in threads if thread.get('commentable_id') in discussion_category_ids ] for thread in threads: # patch for backward compatibility to comments service if 'pinned' not in thread: thread['pinned'] = False query_params['page'] = paginated_results.page query_params['num_pages'] = paginated_results.num_pages query_params['corrected_text'] = paginated_results.corrected_text return threads, query_params
def cohorts_enabled(self): return is_course_cohorted(self.course_id)
def instructor_dashboard(request, course_id): """Display the instructor dashboard for a course.""" course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'staff', course_key, depth=None) instructor_access = bool(has_access(request.user, 'instructor', course)) # an instructor can manage staff lists forum_admin_access = has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR) msg = '' show_email_tab = False problems = [] plots = [] datatable = {} # the instructor dashboard page is modal: grades, psychometrics, admin # keep that state in request.session (defaults to grades mode) idash_mode = request.POST.get('idash_mode', '') idash_mode_key = u'idash_mode:{0}'.format(course_id) if idash_mode: request.session[idash_mode_key] = idash_mode else: idash_mode = request.session.get(idash_mode_key, 'Grades') enrollment_number = CourseEnrollment.objects.num_enrolled_in(course_key) # assemble some course statistics for output to instructor def get_course_stats_table(): datatable = { 'header': ['Statistic', 'Value'], 'title': _('Course Statistics At A Glance'), } data = [['Date', timezone.now().isoformat()]] data += compute_course_stats(course).items() if request.user.is_staff: for field in course.fields.values(): if getattr(field.scope, 'user', False): continue data.append([ field.name, json.dumps(field.read_json(course), cls=i4xEncoder) ]) datatable['data'] = data return datatable def return_csv(func, datatable, file_pointer=None): """Outputs a CSV file from the contents of a datatable.""" if file_pointer is None: response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = (u'attachment; filename={0}'.format(func)).encode('utf-8') else: response = file_pointer writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) encoded_row = [unicode(s).encode('utf-8') for s in datatable['header']] writer.writerow(encoded_row) for datarow in datatable['data']: # 's' here may be an integer, float (eg score) or string (eg student name) encoded_row = [ # If s is already a UTF-8 string, trying to make a unicode # object out of it will fail unless we pass in an encoding to # the constructor. But we can't do that across the board, # because s is often a numeric type. So just do this. s if isinstance(s, str) else unicode(s).encode('utf-8') for s in datarow ] writer.writerow(encoded_row) return response # process actions from form POST action = request.POST.get('action', '') use_offline = request.POST.get('use_offline_grades', False) if settings.FEATURES['ENABLE_MANUAL_GIT_RELOAD']: if 'GIT pull' in action: data_dir = course.data_dir log.debug('git pull %s', data_dir) gdir = settings.DATA_DIR / data_dir if not os.path.exists(gdir): msg += "====> ERROR in gitreload - no such directory {0}".format(gdir) else: cmd = "cd {0}; git reset --hard HEAD; git clean -f -d; git pull origin; chmod g+w course.xml".format(gdir) msg += "git pull on {0}:<p>".format(data_dir) msg += "<pre>{0}</pre></p>".format(escape(os.popen(cmd).read())) track.views.server_track(request, "git-pull", {"directory": data_dir}, page="idashboard") if 'Reload course' in action: log.debug('reloading %s (%s)', course_key, course) try: data_dir = course.data_dir modulestore().try_load_course(data_dir) msg += "<br/><p>Course reloaded from {0}</p>".format(data_dir) track.views.server_track(request, "reload", {"directory": data_dir}, page="idashboard") course_errors = modulestore().get_course_errors(course.id) msg += '<ul>' for cmsg, cerr in course_errors: msg += "<li>{0}: <pre>{1}</pre>".format(cmsg, escape(cerr)) msg += '</ul>' except Exception as err: # pylint: disable=broad-except msg += '<br/><p>Error: {0}</p>'.format(escape(err)) if action == 'Dump list of enrolled students' or action == 'List enrolled students': log.debug(action) datatable = get_student_grade_summary_data(request, course, get_grades=False, use_offline=use_offline) datatable['title'] = _('List of students enrolled in {course_key}').format(course_key=course_key.to_deprecated_string()) track.views.server_track(request, "list-students", {}, page="idashboard") elif 'Dump all RAW grades' in action: log.debug(action) datatable = get_student_grade_summary_data(request, course, get_grades=True, get_raw_scores=True, use_offline=use_offline) datatable['title'] = _('Raw Grades of students enrolled in {course_key}').format(course_key=course_key) track.views.server_track(request, "dump-grades-raw", {}, page="idashboard") elif 'Download CSV of all RAW grades' in action: track.views.server_track(request, "dump-grades-csv-raw", {}, page="idashboard") return return_csv('grades_{0}_raw.csv'.format(course_key.to_deprecated_string()), get_student_grade_summary_data(request, course, get_raw_scores=True, use_offline=use_offline)) elif 'Download CSV of answer distributions' in action: track.views.server_track(request, "dump-answer-dist-csv", {}, page="idashboard") return return_csv('answer_dist_{0}.csv'.format(course_key.to_deprecated_string()), get_answers_distribution(request, course_key)) #---------------------------------------- # export grades to remote gradebook elif action == 'List assignments available in remote gradebook': msg2, datatable = _do_remote_gradebook(request.user, course, 'get-assignments') msg += msg2 elif action == 'List assignments available for this course': log.debug(action) allgrades = get_student_grade_summary_data(request, course, get_grades=True, use_offline=use_offline) assignments = [[x] for x in allgrades['assignments']] datatable = {'header': [_('Assignment Name')]} datatable['data'] = assignments datatable['title'] = action msg += 'assignments=<pre>%s</pre>' % assignments elif action == 'List enrolled students matching remote gradebook': stud_data = get_student_grade_summary_data(request, course, get_grades=False, use_offline=use_offline) msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') datatable = {'header': ['Student email', 'Match?']} rg_students = [x['email'] for x in rg_stud_data['retdata']] def domatch(student): """Returns 'yes' if student is pressent in the remote gradebook student list, else returns 'No'""" return 'yes' if student.email in rg_students else 'No' datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']] datatable['title'] = action elif action in ['Display grades for assignment', 'Export grades for assignment to remote gradebook', 'Export CSV file of grades for assignment']: log.debug(action) datatable = {} aname = request.POST.get('assignment_name', '') if not aname: msg += "<font color='red'>{text}</font>".format(text=_("Please enter an assignment name")) else: allgrades = get_student_grade_summary_data(request, course, get_grades=True, use_offline=use_offline) if aname not in allgrades['assignments']: msg += "<font color='red'>{text}</font>".format( text=_("Invalid assignment name '{name}'").format(name=aname) ) else: aidx = allgrades['assignments'].index(aname) datatable = {'header': [_('External email'), aname]} ddata = [] for student in allgrades['students']: # do one by one in case there is a student who has only partial grades try: ddata.append([student.email, student.grades[aidx]]) except IndexError: log.debug(u'No grade for assignment %(idx)s (%(name)s) for student %(email)s', { "idx": aidx, "name": aname, "email": student.email, }) datatable['data'] = ddata datatable['title'] = _('Grades for assignment "{name}"').format(name=aname) if 'Export CSV' in action: # generate and return CSV file return return_csv('grades {name}.csv'.format(name=aname), datatable) elif 'remote gradebook' in action: file_pointer = StringIO() return_csv('', datatable, file_pointer=file_pointer) file_pointer.seek(0) files = {'datafile': file_pointer} msg2, __ = _do_remote_gradebook(request.user, course, 'post-grades', files=files) msg += msg2 #---------------------------------------- # enrollment elif action == 'Enroll multiple students': is_shib_course = uses_shib(course) students = request.POST.get('multiple_students', '') auto_enroll = bool(request.POST.get('auto_enroll')) email_students = bool(request.POST.get('email_students')) secure = request.is_secure() ret = _do_enroll_students(course, course_key, students, secure=secure, auto_enroll=auto_enroll, email_students=email_students, is_shib_course=is_shib_course) datatable = ret['datatable'] elif action == 'Unenroll multiple students': students = request.POST.get('multiple_students', '') email_students = bool(request.POST.get('email_students')) ret = _do_unenroll_students(course_key, students, email_students=email_students) datatable = ret['datatable'] elif action == 'List sections available in remote gradebook': msg2, datatable = _do_remote_gradebook(request.user, course, 'get-sections') msg += msg2 elif action in ['List students in section in remote gradebook', 'Overload enrollment list using remote gradebook', 'Merge enrollment list with remote gradebook']: section = request.POST.get('gradebook_section', '') msg2, datatable = _do_remote_gradebook(request.user, course, 'get-membership', dict(section=section)) msg += msg2 if 'List' not in action: students = ','.join([x['email'] for x in datatable['retdata']]) overload = 'Overload' in action secure = request.is_secure() ret = _do_enroll_students(course, course_key, students, secure=secure, overload=overload) datatable = ret['datatable'] #---------------------------------------- # psychometrics elif action == 'Generate Histogram and IRT Plot': problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg track.views.server_track(request, "psychometrics-histogram-generation", {"problem": unicode(problem)}, page="idashboard") if idash_mode == 'Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_key) #---------------------------------------- # analytics def get_analytics_result(analytics_name): """Return data for an Analytic piece, or None if it doesn't exist. It logs and swallows errors. """ url = settings.ANALYTICS_SERVER_URL + \ u"get?aname={}&course_id={}&apikey={}".format( analytics_name, urllib.quote(unicode(course_key)), settings.ANALYTICS_API_KEY ) try: res = requests.get(url) except Exception: # pylint: disable=broad-except log.exception("Error trying to access analytics at %s", url) return None if res.status_code == codes.OK: # WARNING: do not use req.json because the preloaded json doesn't # preserve the order of the original record (hence OrderedDict). payload = json.loads(res.content, object_pairs_hook=OrderedDict) add_block_ids(payload) return payload else: log.error("Error fetching %s, code: %s, msg: %s", url, res.status_code, res.content) return None analytics_results = {} if idash_mode == 'Analytics': dashboard_analytics = [ # "StudentsAttemptedProblems", # num students who tried given problem "StudentsDailyActivity", # active students by day "StudentsDropoffPerDay", # active students dropoff by day # "OverallGradeDistribution", # overall point distribution for course # "StudentsPerProblemCorrect", # foreach problem, num students correct "ProblemGradeDistribution", # foreach problem, grade distribution ] for analytic_name in dashboard_analytics: analytics_results[analytic_name] = get_analytics_result(analytic_name) #---------------------------------------- # Metrics metrics_results = {} if settings.FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics': metrics_results['section_display_name'] = dashboard_data.get_section_display_name(course_key) metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(course_key) #---------------------------------------- # offline grades? if use_offline: msg += "<br/><font color='orange'>{text}</font>".format( text=_("Grades from {course_id}").format( course_id=offline_grades_available(course_key) ) ) # generate list of pending background tasks if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): instructor_tasks = get_running_instructor_tasks(course_key) else: instructor_tasks = None # determine if this is a studio-backed course so we can provide a link to edit this course in studio is_studio_course = modulestore().get_modulestore_type(course_key) != ModuleStoreEnum.Type.xml studio_url = None if is_studio_course: studio_url = get_cms_course_link(course) if bulk_email_is_enabled_for_course(course_key): show_email_tab = True # display course stats only if there is no other table to display: course_stats = None if not datatable: course_stats = get_course_stats_table() # disable buttons for large courses disable_buttons = False max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") if max_enrollment_for_buttons is not None: disable_buttons = enrollment_number > max_enrollment_for_buttons #---------------------------------------- # context for rendering context = { 'course': course, 'course_is_cohorted': is_course_cohorted(course.id), 'staff_access': True, 'admin_access': request.user.is_staff, 'instructor_access': instructor_access, 'forum_admin_access': forum_admin_access, 'datatable': datatable, 'course_stats': course_stats, 'msg': msg, 'modeflag': {idash_mode: 'selectedmode'}, 'studio_url': studio_url, 'show_email_tab': show_email_tab, # email 'problems': problems, # psychometrics 'plots': plots, # psychometrics 'course_errors': modulestore().get_course_errors(course.id), 'instructor_tasks': instructor_tasks, 'offline_grade_log': offline_grades_available(course_key), 'analytics_results': analytics_results, 'disable_buttons': disable_buttons, 'metrics_results': metrics_results, } context['standard_dashboard_url'] = reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()}) return render_to_response('courseware/legacy_instructor_dashboard.html', context)
def post(self, request): username = request.DATA.get('username') try: user = User.objects.get(username=username) except ObjectDoesNotExist: return Response( status=status.HTTP_400_BAD_REQUEST, data={"message": u"User {username} does not exist".format(username=username)} ) course_id = request.DATA.get('course_id') if not course_id: return Response( status=status.HTTP_400_BAD_REQUEST, data={"message": u"Course ID must be specified to create a new enrollment."} ) try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"No course '{course_id}' found for enrollment".format(course_id=course_id) } ) course_is_cohorted = is_course_cohorted(course_key) if not course_is_cohorted: return Response( status=status.HTTP_200_OK, data={"message": u"Course {course_id} is not cohorted.".format(course_id=course_id)} ) action = request.DATA.get('action') if action not in [u'add', u'delete']: return Response( status=status.HTTP_400_BAD_REQUEST, data={"message": u"Available actions are 'add' and 'delete'."} ) cohort_exists = is_cohort_exists(course_key, VERIFIED) if not cohort_exists: if action == u'add': cohort = add_cohort(course_key, VERIFIED, 'manual') else: return Response( status=status.HTTP_200_OK, data={"message": u"There aren't cohort verified for {course_id}".format(course_id=course_id)} ) else: cohort = get_cohort_by_name(course_key, VERIFIED) enrollment = CourseEnrollment.objects.get( user__username=username, course_id=course_key ) if not enrollment or not enrollment.is_active: if action == u'add': return Response( status=status.HTTP_400_BAD_REQUEST, data={"message": u"User {username} not enrolled or unenrolled in course {course_id}.".format( username=username, course_id=course_id )} ) if action == u'delete': return Response( status=status.HTTP_200_OK, data={"message": u"User {username} not enrolled or unenrolled in course {course_id}.".format( username=username, course_id=course_id )} ) course_cohorts = CourseUserGroup.objects.filter( course_id=course_key, users__id=user.id, group_type=CourseUserGroup.COHORT ) # remove user from verified cohort if action == u'delete': if not course_cohorts.exists() or course_cohorts[0].name != cohort.name: return Response( status=status.HTTP_200_OK, data={"message": u"User {username} already was removed from cohort {cohort_name}".format( username=username, cohort_name=cohort.name )} ) else: cohort.users.remove(user) return Response( status=status.HTTP_200_OK, data={"message": u"User {username} removed from cohort {cohort_name}".format( username=username, cohort_name=cohort.name )} ) if course_cohorts.exists(): if course_cohorts[0] == cohort: return Response( status=status.HTTP_200_OK, data={"message": u"User {username} already present in cohort {cohort_name}".format( username=username, cohort_name=cohort.name )} ) add_user_into_verified_cohort(course_cohorts, cohort, user) return Response( status=status.HTTP_200_OK, data={"message": u"User {username} added to cohort {cohort_name}".format( username=user.username, cohort_name=cohort.name )} )
def task_generate_xls(self): log.info('Start task generate XLS') #Get report infos self.microsite = self.request.get('microsite') report_fields = self.request.get('form') register_fields = self.request.get('register_form') certificate_fields = self.request.get('certificate_form') course_key = CourseKey.from_string(self.course_id) course = get_course_by_id(course_key) form_factory = ensure_form_factory() form_factory.connect(db='ensure_form', collection='certificate_form') #Dict of labels form_labels = { "last_connexion": _("Last login"), "inscription_date": _("Register date"), "user_id": _("User id"), "email": _("Email"), "grade_final": _("Final Grade"), "cohorte_names": _("Cohorte name"), "time_tracking": _("Time spent"), "certified": _("Attestation"), "username": _("Username"), } for field in register_fields: form_labels[field.get('name')] = field.get('label') for field in certificate_fields: form_labels[field.get('name')] = field.get('label') #Identify multiple cells fields multiple_cell_fields = ["exercises_grade", "grade_detailed"] #Is report cohort specific? course_cohorted = is_course_cohorted(course_key) if course_cohorted: cohortes_targeted = [ field.replace('cohort_selection_', '') for field in report_fields if field.find('cohort_selection_') > -1 ] for field in report_fields: log.info(field) log.info(field.find('cohort_selection_')) log.info(cohortes_targeted) log.info('cohortes_targeted') if cohortes_targeted and not 'cohorte_names' in report_fields: report_fields.append('cohorte_names') else: if 'cohorte_names' in report_fields: report_fields.remove('cohorte_names') #Get Graded block for exercises_grade details graded_scorable_blocks = self.tma_graded_scorable_blocks_to_header( course_key) #Create Workbook wb = Workbook(encoding='utf-8') filename = '/home/edxtma/csv/{}_{}.xls'.format( time.strftime("%Y_%m_%d"), course.display_name_with_default) sheet = wb.add_sheet('Grade Report') #Write information line = 1 course_enrollments = CourseEnrollment.objects.filter( course_id=course_key, is_active=1) for enrollment in course_enrollments: #do not include in reports if not active if not enrollment.is_active: continue #Gather user information user = enrollment.user user_grade = CourseGradeFactory().create(user, course) grade_summary = {} if course_cohorted: user_cohorte = get_cohort(user, course_key).name #if cohort specific report avoid student that are not part of cohortes_targeted provided if cohortes_targeted and not user_cohorte in cohortes_targeted: continue for section_grade in user_grade.grade_value['section_breakdown']: grade_summary[ section_grade['category']] = section_grade['percent'] try: custom_field = json.loads( UserProfile.objects.get(user=user).custom_field) except: custom_field = {} user_certificate_info = {} try: form_factory.microsite = self.microsite form_factory.user_id = user.id user_certificate_info = form_factory.getForm( user_id=True, microsite=True).get('form') except: pass cell = 0 for field in report_fields: if field in multiple_cell_fields: if field == "grade_detailed": for section in grade_summary: section_grade = str( int(round(grade_summary[section] * 100))) + '%' sheet.write(line, cell, section_grade) #Write header if line == 1: sheet.write(0, cell, "Travail - " + section) cell += 1 elif field == "exercises_grade": for block_location in graded_scorable_blocks.items(): try: problem_score = user_grade.locations_to_scores[ block_location[0]] if problem_score.attempted: value = round( float(problem_score.earned) / problem_score.possible, 2) else: value = _('n.a.') except: value = _('inv.') sheet.write(line, cell, value) if line == 1: sheet.write(0, cell, block_location[1]) cell += 1 else: value = '' if field == "user_id": value = user.id elif field == "email": value = user.email elif field == "first_name": try: if user.first_name: value = user.first_name elif custom_field: value = custom_field.get( 'first_name', 'unkowna') else: value = 'unknown' except: value = 'unknown' elif field == "last_name": try: if user.last_name: value = user.last_name elif custom_field: value = custom_field.get( 'last_name', 'unkowna') except: value = 'unknown' elif field == "last_connexion": try: value = user.last_login.strftime('%d-%m-%y') except: value = '' elif field == "inscription_date": try: value = user.date_joined.strftime('%d-%m-%y') except: value = '' elif field == "cohorte_names": try: value = user_cohorte except: value = '' elif field == "time_tracking": value = self.get_time_tracking(enrollment) elif field == "certified": if user_grade.passed: value = _("Yes") else: value = _("No") elif field == "grade_final": value = str(int(round(user_grade.percent * 100))) + '%' elif field == "username": value = user.username elif field in user_certificate_info.keys(): value = user_certificate_info.get(field) else: value = custom_field.get(field, '') #Write header and write value log.info('field') log.info(field) log.info('value') log.info(value) log.info(form_labels) if field in form_labels.keys(): sheet.write(line, cell, value) if line == 1: sheet.write(0, cell, form_labels.get(field)) cell += 1 line += 1 log.warning("file ok") #Save the file output = BytesIO() wb.save(output) _files_values = output.getvalue() log.warning("file saved") #Send the email to receivers receivers = self.request.get('send_to') html = "<html><head></head><body><p>Bonjour,<br/><br/>Vous trouverez en PJ le rapport de donnees du MOOC {}<br/><br/>Bonne reception<br>The MOOC Agency<br></p></body></html>".format( course.display_name) part2 = MIMEText(html.encode('utf-8'), 'html', 'utf-8') for receiver in receivers: fromaddr = "*****@*****.**" toaddr = str(receiver) msg = MIMEMultipart() msg['From'] = fromaddr msg['To'] = toaddr msg['Subject'] = "Rapport de donnees" attachment = _files_values part = MIMEBase('application', 'octet-stream') part.set_payload(attachment) encoders.encode_base64(part) part.add_header( 'Content-Disposition', "attachment; filename= %s" % os.path.basename(filename)) msg.attach(part) server = smtplib.SMTP('mail3.themoocagency.com', 25) server.starttls() server.login('contact', 'waSwv6Eqer89') msg.attach(part2) text = msg.as_string() server.sendmail(fromaddr, toaddr, text) server.quit() log.warning("file sent to {}".format(receiver)) response = {'path': self.filename, 'send_to': receivers} return response
def instructor_dashboard(request, course_id): """Display the instructor dashboard for a course.""" course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'staff', course_key, depth=None) instructor_access = has_access(request.user, 'instructor', course) # an instructor can manage staff lists forum_admin_access = has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR) msg = '' show_email_tab = False problems = [] plots = [] datatable = {} # the instructor dashboard page is modal: grades, psychometrics, admin # keep that state in request.session (defaults to grades mode) idash_mode = request.POST.get('idash_mode', '') idash_mode_key = u'idash_mode:{0}'.format(course_id) if idash_mode: request.session[idash_mode_key] = idash_mode else: idash_mode = request.session.get(idash_mode_key, 'Grades') enrollment_number = CourseEnrollment.num_enrolled_in(course_key) # assemble some course statistics for output to instructor def get_course_stats_table(): datatable = { 'header': ['Statistic', 'Value'], 'title': _('Course Statistics At A Glance'), } data = [['Date', timezone.now().isoformat()]] data += compute_course_stats(course).items() if request.user.is_staff: for field in course.fields.values(): if getattr(field.scope, 'user', False): continue data.append([ field.name, json.dumps(field.read_json(course), cls=i4xEncoder) ]) datatable['data'] = data return datatable def return_csv(func, datatable, file_pointer=None): """Outputs a CSV file from the contents of a datatable.""" if file_pointer is None: response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = (u'attachment; filename={0}'.format(func)).encode('utf-8') else: response = file_pointer writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) encoded_row = [unicode(s).encode('utf-8') for s in datatable['header']] writer.writerow(encoded_row) for datarow in datatable['data']: # 's' here may be an integer, float (eg score) or string (eg student name) encoded_row = [ # If s is already a UTF-8 string, trying to make a unicode # object out of it will fail unless we pass in an encoding to # the constructor. But we can't do that across the board, # because s is often a numeric type. So just do this. s if isinstance(s, str) else unicode(s).encode('utf-8') for s in datarow ] writer.writerow(encoded_row) return response # process actions from form POST action = request.POST.get('action', '') use_offline = request.POST.get('use_offline_grades', False) if settings.FEATURES['ENABLE_MANUAL_GIT_RELOAD']: if 'GIT pull' in action: data_dir = course.data_dir log.debug('git pull {0}'.format(data_dir)) gdir = settings.DATA_DIR / data_dir if not os.path.exists(gdir): msg += "====> ERROR in gitreload - no such directory {0}".format(gdir) else: cmd = "cd {0}; git reset --hard HEAD; git clean -f -d; git pull origin; chmod g+w course.xml".format(gdir) msg += "git pull on {0}:<p>".format(data_dir) msg += "<pre>{0}</pre></p>".format(escape(os.popen(cmd).read())) track.views.server_track(request, "git-pull", {"directory": data_dir}, page="idashboard") if 'Reload course' in action: log.debug('reloading {0} ({1})'.format(course_key, course)) try: data_dir = course.data_dir modulestore().try_load_course(data_dir) msg += "<br/><p>Course reloaded from {0}</p>".format(data_dir) track.views.server_track(request, "reload", {"directory": data_dir}, page="idashboard") course_errors = modulestore().get_course_errors(course.id) msg += '<ul>' for cmsg, cerr in course_errors: msg += "<li>{0}: <pre>{1}</pre>".format(cmsg, escape(cerr)) msg += '</ul>' except Exception as err: # pylint: disable=broad-except msg += '<br/><p>Error: {0}</p>'.format(escape(err)) if action == 'Dump list of enrolled students' or action == 'List enrolled students': log.debug(action) datatable = get_student_grade_summary_data(request, course, get_grades=False, use_offline=use_offline) datatable['title'] = _('List of students enrolled in {course_key}').format(course_key=course_key.to_deprecated_string()) track.views.server_track(request, "list-students", {}, page="idashboard") elif 'Dump all RAW grades' in action: log.debug(action) datatable = get_student_grade_summary_data(request, course, get_grades=True, get_raw_scores=True, use_offline=use_offline) datatable['title'] = _('Raw Grades of students enrolled in {course_key}').format(course_key=course_key) track.views.server_track(request, "dump-grades-raw", {}, page="idashboard") elif 'Download CSV of all RAW grades' in action: track.views.server_track(request, "dump-grades-csv-raw", {}, page="idashboard") return return_csv('grades_{0}_raw.csv'.format(course_key.to_deprecated_string()), get_student_grade_summary_data(request, course, get_raw_scores=True, use_offline=use_offline)) elif 'Download CSV of answer distributions' in action: track.views.server_track(request, "dump-answer-dist-csv", {}, page="idashboard") return return_csv('answer_dist_{0}.csv'.format(course_key.to_deprecated_string()), get_answers_distribution(request, course_key)) #---------------------------------------- # export grades to remote gradebook elif action == 'List assignments available in remote gradebook': msg2, datatable = _do_remote_gradebook(request.user, course, 'get-assignments') msg += msg2 elif action == 'List assignments available for this course': log.debug(action) allgrades = get_student_grade_summary_data(request, course, get_grades=True, use_offline=use_offline) assignments = [[x] for x in allgrades['assignments']] datatable = {'header': [_('Assignment Name')]} datatable['data'] = assignments datatable['title'] = action msg += 'assignments=<pre>%s</pre>' % assignments elif action == 'List enrolled students matching remote gradebook': stud_data = get_student_grade_summary_data(request, course, get_grades=False, use_offline=use_offline) msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') datatable = {'header': ['Student email', 'Match?']} rg_students = [x['email'] for x in rg_stud_data['retdata']] def domatch(student): """Returns 'yes' if student is pressent in the remote gradebook student list, else returns 'No'""" return 'yes' if student.email in rg_students else 'No' datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']] datatable['title'] = action elif action in ['Display grades for assignment', 'Export grades for assignment to remote gradebook', 'Export CSV file of grades for assignment']: log.debug(action) datatable = {} aname = request.POST.get('assignment_name', '') if not aname: msg += "<font color='red'>{text}</font>".format(text=_("Please enter an assignment name")) else: allgrades = get_student_grade_summary_data(request, course, get_grades=True, use_offline=use_offline) if aname not in allgrades['assignments']: msg += "<font color='red'>{text}</font>".format( text=_("Invalid assignment name '{name}'").format(name=aname) ) else: aidx = allgrades['assignments'].index(aname) datatable = {'header': [_('External email'), aname]} ddata = [] for student in allgrades['students']: # do one by one in case there is a student who has only partial grades try: ddata.append([student.email, student.grades[aidx]]) except IndexError: log.debug(u'No grade for assignment %(idx)s (%(name)s) for student %(email)s', { "idx": aidx, "name": aname, "email": student.email, }) datatable['data'] = ddata datatable['title'] = _('Grades for assignment "{name}"').format(name=aname) if 'Export CSV' in action: # generate and return CSV file return return_csv('grades {name}.csv'.format(name=aname), datatable) elif 'remote gradebook' in action: file_pointer = StringIO() return_csv('', datatable, file_pointer=file_pointer) file_pointer.seek(0) files = {'datafile': file_pointer} msg2, __ = _do_remote_gradebook(request.user, course, 'post-grades', files=files) msg += msg2 #---------------------------------------- # DataDump elif 'Download CSV of all responses to problem' in action: problem_to_dump = request.POST.get('problem_to_dump', '') if problem_to_dump[-4:] == ".xml": problem_to_dump = problem_to_dump[:-4] try: module_state_key = course_key.make_usage_key_from_deprecated_string(problem_to_dump) smdat = StudentModule.objects.filter( course_id=course_key, module_state_key=module_state_key ) smdat = smdat.order_by('student') msg += _("Found {num} records to dump.").format(num=smdat) except Exception as err: # pylint: disable=broad-except msg += "<font color='red'>{text}</font><pre>{err}</pre>".format( text=_("Couldn't find module with that urlname."), err=escape(err) ) smdat = [] if smdat: datatable = {'header': ['username', 'state']} datatable['data'] = [[x.student.username, x.state] for x in smdat] datatable['title'] = _('Student state for problem {problem}').format(problem=problem_to_dump) return return_csv('student_state_from_{problem}.csv'.format(problem=problem_to_dump), datatable) #---------------------------------------- # enrollment elif action == 'List students who may enroll but may not have yet signed up': ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key) datatable = {'header': ['StudentEmail']} datatable['data'] = [[x.email] for x in ceaset] datatable['title'] = action elif action == 'Enroll multiple students': is_shib_course = uses_shib(course) students = request.POST.get('multiple_students', '') auto_enroll = bool(request.POST.get('auto_enroll')) email_students = bool(request.POST.get('email_students')) secure = request.is_secure() ret = _do_enroll_students(course, course_key, students, secure=secure, auto_enroll=auto_enroll, email_students=email_students, is_shib_course=is_shib_course) datatable = ret['datatable'] elif action == 'Unenroll multiple students': students = request.POST.get('multiple_students', '') email_students = bool(request.POST.get('email_students')) ret = _do_unenroll_students(course_key, students, email_students=email_students) datatable = ret['datatable'] elif action == 'List sections available in remote gradebook': msg2, datatable = _do_remote_gradebook(request.user, course, 'get-sections') msg += msg2 elif action in ['List students in section in remote gradebook', 'Overload enrollment list using remote gradebook', 'Merge enrollment list with remote gradebook']: section = request.POST.get('gradebook_section', '') msg2, datatable = _do_remote_gradebook(request.user, course, 'get-membership', dict(section=section)) msg += msg2 if 'List' not in action: students = ','.join([x['email'] for x in datatable['retdata']]) overload = 'Overload' in action secure = request.is_secure() ret = _do_enroll_students(course, course_key, students, secure=secure, overload=overload) datatable = ret['datatable'] #---------------------------------------- # psychometrics elif action == 'Generate Histogram and IRT Plot': problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg track.views.server_track(request, "psychometrics-histogram-generation", {"problem": unicode(problem)}, page="idashboard") if idash_mode == 'Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_key) #---------------------------------------- # analytics def get_analytics_result(analytics_name): """Return data for an Analytic piece, or None if it doesn't exist. It logs and swallows errors. """ url = settings.ANALYTICS_SERVER_URL + \ u"get?aname={}&course_id={}&apikey={}".format( analytics_name, urllib.quote(unicode(course_key)), settings.ANALYTICS_API_KEY ) try: res = requests.get(url) except Exception: # pylint: disable=broad-except log.exception("Error trying to access analytics at %s", url) return None if res.status_code == codes.OK: # WARNING: do not use req.json because the preloaded json doesn't # preserve the order of the original record (hence OrderedDict). payload = json.loads(res.content, object_pairs_hook=OrderedDict) add_block_ids(payload) return payload else: log.error("Error fetching %s, code: %s, msg: %s", url, res.status_code, res.content) return None analytics_results = {} if idash_mode == 'Analytics': dashboard_analytics = [ # "StudentsAttemptedProblems", # num students who tried given problem "StudentsDailyActivity", # active students by day "StudentsDropoffPerDay", # active students dropoff by day # "OverallGradeDistribution", # overall point distribution for course # "StudentsPerProblemCorrect", # foreach problem, num students correct "ProblemGradeDistribution", # foreach problem, grade distribution ] for analytic_name in dashboard_analytics: analytics_results[analytic_name] = get_analytics_result(analytic_name) #---------------------------------------- # Metrics metrics_results = {} if settings.FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics': metrics_results['section_display_name'] = dashboard_data.get_section_display_name(course_key) metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(course_key) #---------------------------------------- # offline grades? if use_offline: msg += "<br/><font color='orange'>{text}</font>".format( text=_("Grades from {course_id}").format( course_id=offline_grades_available(course_key) ) ) # generate list of pending background tasks if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): instructor_tasks = get_running_instructor_tasks(course_key) else: instructor_tasks = None # determine if this is a studio-backed course so we can provide a link to edit this course in studio is_studio_course = modulestore().get_modulestore_type(course_key) != ModuleStoreEnum.Type.xml studio_url = None if is_studio_course: studio_url = get_cms_course_link(course) if bulk_email_is_enabled_for_course(course_key): show_email_tab = True # display course stats only if there is no other table to display: course_stats = None if not datatable: course_stats = get_course_stats_table() # disable buttons for large courses disable_buttons = False max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") if max_enrollment_for_buttons is not None: disable_buttons = enrollment_number > max_enrollment_for_buttons #---------------------------------------- # context for rendering context = { 'course': course, 'course_is_cohorted': is_course_cohorted(course.id), 'staff_access': True, 'admin_access': request.user.is_staff, 'instructor_access': instructor_access, 'forum_admin_access': forum_admin_access, 'datatable': datatable, 'course_stats': course_stats, 'msg': msg, 'modeflag': {idash_mode: 'selectedmode'}, 'studio_url': studio_url, 'show_email_tab': show_email_tab, # email 'problems': problems, # psychometrics 'plots': plots, # psychometrics 'course_errors': modulestore().get_course_errors(course.id), 'instructor_tasks': instructor_tasks, 'offline_grade_log': offline_grades_available(course_key), 'analytics_results': analytics_results, 'disable_buttons': disable_buttons, 'metrics_results': metrics_results, } context['standard_dashboard_url'] = reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()}) return render_to_response('courseware/legacy_instructor_dashboard.html', context)
def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): # pylint: disable=too-many-statements """ For a given `course_id`, generate a grades CSV file for all students that are enrolled, and store using a `ReportStore`. Once created, the files can be accessed by instantiating another `ReportStore` (via `ReportStore.from_config()`) and calling `link_for()` on it. Writes are buffered, so we'll never write part of a CSV file to S3 -- i.e. any files that are visible in ReportStore will be complete ones. As we start to add more CSV downloads, it will probably be worthwhile to make a more general CSVDoc class instead of building out the rows like we do here. """ start_time = time() start_date = datetime.now(UTC) status_interval = 100 enrolled_students = CourseEnrollment.objects.users_enrolled_in(course_id) task_progress = TaskProgress(action_name, enrolled_students.count(), start_time) fmt = u'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}' task_info_string = fmt.format( task_id=_xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None, entry_id=_entry_id, course_id=course_id, task_input=_task_input ) TASK_LOG.info(u'%s, Task type: %s, Starting task execution', task_info_string, action_name) course = get_course_by_id(course_id) course_is_cohorted = is_course_cohorted(course.id) cohorts_header = ['Cohort Name'] if course_is_cohorted else [] experiment_partitions = get_split_user_partitions(course.user_partitions) group_configs_header = [u'Experiment Group ({})'.format(partition.name) for partition in experiment_partitions] certificate_info_header = ['Certificate Eligible', 'Certificate Delivered', 'Certificate Type'] certificate_whitelist = CertificateWhitelist.objects.filter(course_id=course_id, whitelist=True) whitelisted_user_ids = [entry.user_id for entry in certificate_whitelist] # Loop over all our students and build our CSV lists in memory header = None rows = [] err_rows = [["id", "username", "error_msg"]] current_step = {'step': 'Calculating Grades'} total_enrolled_students = enrolled_students.count() student_counter = 0 TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Starting grade calculation for total students: %s', task_info_string, action_name, current_step, total_enrolled_students ) for student, gradeset, err_msg in iterate_grades_for(course_id, enrolled_students): # Periodically update task status (this is a cache write) if task_progress.attempted % status_interval == 0: task_progress.update_task_state(extra_meta=current_step) task_progress.attempted += 1 # Now add a log entry after each student is graded to get a sense # of the task's progress student_counter += 1 TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Grade calculation in-progress for students: %s/%s', task_info_string, action_name, current_step, student_counter, total_enrolled_students ) if gradeset: # We were able to successfully grade this student for this course. task_progress.succeeded += 1 if not header: header = [section['label'] for section in gradeset[u'section_breakdown']] rows.append( ["id", "email", "username", "grade"] + header + cohorts_header + group_configs_header + ['Enrollment Track', 'Verification Status'] + certificate_info_header ) percents = { section['label']: section.get('percent', 0.0) for section in gradeset[u'section_breakdown'] if 'label' in section } cohorts_group_name = [] if course_is_cohorted: group = get_cohort(student, course_id, assign=False) cohorts_group_name.append(group.name if group else '') group_configs_group_names = [] for partition in experiment_partitions: group = LmsPartitionService(student, course_id).get_group(partition, assign=False) group_configs_group_names.append(group.name if group else '') enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)[0] verification_status = SoftwareSecurePhotoVerification.verification_status_for_user( student, course_id, enrollment_mode ) certificate_info = certificate_info_for_user( student, course_id, gradeset['grade'], student.id in whitelisted_user_ids ) # Not everybody has the same gradable items. If the item is not # found in the user's gradeset, just assume it's a 0. The aggregated # grades for their sections and overall course will be calculated # without regard for the item they didn't have access to, so it's # possible for a student to have a 0.0 show up in their row but # still have 100% for the course. row_percents = [percents.get(label, 0.0) for label in header] rows.append( [student.id, student.email, student.username, gradeset['percent']] + row_percents + cohorts_group_name + group_configs_group_names + [enrollment_mode] + [verification_status] + certificate_info ) else: # An empty gradeset means we failed to grade a student. task_progress.failed += 1 err_rows.append([student.id, student.username, err_msg]) TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Grade calculation completed for students: %s/%s', task_info_string, action_name, current_step, student_counter, total_enrolled_students ) # By this point, we've got the rows we're going to stuff into our CSV files. current_step = {'step': 'Uploading CSVs'} task_progress.update_task_state(extra_meta=current_step) TASK_LOG.info(u'%s, Task type: %s, Current step: %s', task_info_string, action_name, current_step) # Perform the actual upload upload_csv_to_report_store(rows, 'grade_report', course_id, start_date) # If there are any error rows (don't count the header), write them out as well if len(err_rows) > 1: upload_csv_to_report_store(err_rows, 'grade_report_err', course_id, start_date) # One last update before we close out... TASK_LOG.info(u'%s, Task type: %s, Finalizing grade task', task_info_string, action_name) return task_progress.update_task_state(extra_meta=current_step)
def forum_form_discussion(request, course_key): """ Renders the main Discussion page, potentially filtered by a search query """ nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course_settings = make_course_settings(course, request.user) user = cc.User.from_django_user(request.user) user_info = user.to_dict() try: unsafethreads, query_params = get_threads(request, course, user_info) # This might process a search query is_staff = has_permission(request.user, 'openclose_thread', course.id) threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads] except cc.utils.CommentClientMaintenanceError: log.warning("Forum is in maintenance mode") return render_to_response('discussion/maintenance.html', { 'disable_courseware_js': True, 'uses_pattern_library': True, }) except ValueError: return HttpResponseBadRequest("Invalid group_id") with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) if request.is_ajax(): return utils.JsonResponse({ 'discussion_data': threads, # TODO: Standardize on 'discussion_data' vs 'threads' 'annotated_content_info': annotated_content_info, 'num_pages': query_params['num_pages'], 'page': query_params['page'], 'corrected_text': query_params['corrected_text'], }) else: with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): user_cohort_id = get_cohort_id(request.user, course_key) context = { 'csrf': csrf(request)['csrf_token'], 'course': course, #'recent_active_threads': recent_active_threads, 'staff_access': bool(has_access(request.user, 'staff', course)), 'threads': threads, 'thread_pages': query_params['num_pages'], 'user_info': user_info, 'can_create_comment': has_permission(request.user, "create_comment", course.id), 'can_create_subcomment': has_permission(request.user, "create_sub_comment", course.id), 'can_create_thread': has_permission(request.user, "create_thread", course.id), 'flag_moderator': bool( has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course) ), 'annotated_content_info': annotated_content_info, 'course_id': course.id.to_deprecated_string(), 'roles': utils.get_role_ids(course_key), 'is_moderator': has_permission(request.user, "see_all_cohorts", course_key), 'cohorts': course_settings["cohorts"], # still needed to render _thread_list_template 'user_cohort': user_cohort_id, # read from container in NewPostView 'is_course_cohorted': is_course_cohorted(course_key), # still needed to render _thread_list_template 'sort_preference': user.default_sort_key, 'category_map': course_settings["category_map"], 'course_settings': course_settings, 'disable_courseware_js': True, 'uses_pattern_library': True, } # print "start rendering.." return render_to_response('discussion/discussion_board.html', context)
def single_thread(request, course_key, discussion_id, thread_id): """ Renders a response to display a single discussion thread. """ nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course_settings = make_course_settings(course, request.user) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() is_moderator = has_permission(request.user, "see_all_cohorts", course_key) # Currently, the front end always loads responses via AJAX, even for this # page; it would be a nice optimization to avoid that extra round trip to # the comments service. try: thread = cc.Thread.find(thread_id).retrieve( recursive=request.is_ajax(), user_id=request.user.id, response_skip=request.GET.get("resp_skip"), response_limit=request.GET.get("resp_limit")) except cc.utils.CommentClientRequestError as e: if e.status_code == 404: raise Http404 raise # Verify that the student has access to this thread if belongs to a course discussion module thread_context = getattr(thread, "context", "course") if thread_context == "course" and not utils.discussion_category_id_access( course, request.user, discussion_id): raise Http404 # verify that the thread belongs to the requesting student's cohort if is_commentable_cohorted(course_key, discussion_id) and not is_moderator: user_group_id = get_cohort_id(request.user, course_key) if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id: raise Http404 is_staff = has_permission(request.user, 'openclose_thread', course.id) if request.is_ajax(): with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): annotated_content_info = utils.get_annotated_content_infos( course_key, thread, request.user, user_info=user_info) content = utils.prepare_content(thread.to_dict(), course_key, is_staff) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context([content], course, request.user) return utils.JsonResponse({ 'content': content, 'annotated_content_info': annotated_content_info, }) else: try: threads, query_params = get_threads(request, course) except ValueError: return HttpResponseBadRequest("Invalid group_id") threads.append(thread.to_dict()) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) for thread in threads: # patch for backward compatibility with comments service if "pinned" not in thread: thread["pinned"] = False threads = [ utils.prepare_content(thread, course_key, is_staff) for thread in threads ] with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads( course_key, threads, request.user, user_info) with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): user_cohort = get_cohort_id(request.user, course_key) context = { 'discussion_id': discussion_id, 'csrf': csrf(request)['csrf_token'], 'init': '', # TODO: What is this? 'user_info': json.dumps(user_info), 'can_create_comment': json.dumps( has_permission(request.user, "create_comment", course.id)), 'can_create_subcomment': json.dumps( has_permission(request.user, "create_sub_comment", course.id)), 'can_create_thread': has_permission(request.user, "create_thread", course.id), 'annotated_content_info': json.dumps(annotated_content_info), 'course': course, #'recent_active_threads': recent_active_threads, 'course_id': course.id.to_deprecated_string( ), # TODO: Why pass both course and course.id to template? 'thread_id': thread_id, 'threads': json.dumps(threads), 'roles': json.dumps(utils.get_role_ids(course_key)), 'is_moderator': is_moderator, 'thread_pages': query_params['num_pages'], 'is_course_cohorted': is_course_cohorted(course_key), 'flag_moderator': bool( has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course)), 'cohorts': course_settings["cohorts"], 'user_cohort': user_cohort, 'sort_preference': cc_user.default_sort_key, 'category_map': course_settings["category_map"], 'course_settings': json.dumps(course_settings) } return render_to_response('discussion/index.html', context)
def single_thread(request, course_key, discussion_id, thread_id): """ Renders a response to display a single discussion thread. """ nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course_settings = make_course_settings(course, request.user) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() is_moderator = has_permission(request.user, "see_all_cohorts", course_key) # Currently, the front end always loads responses via AJAX, even for this # page; it would be a nice optimization to avoid that extra round trip to # the comments service. try: thread = cc.Thread.find(thread_id).retrieve( recursive=request.is_ajax(), user_id=request.user.id, response_skip=request.GET.get("resp_skip"), response_limit=request.GET.get("resp_limit") ) except cc.utils.CommentClientRequestError as error: if error.status_code == 404: raise Http404 raise # Verify that the student has access to this thread if belongs to a course discussion module thread_context = getattr(thread, "context", "course") if thread_context == "course" and not utils.discussion_category_id_access(course, request.user, discussion_id): raise Http404 # verify that the thread belongs to the requesting student's cohort if is_commentable_cohorted(course_key, discussion_id) and not is_moderator: user_group_id = get_cohort_id(request.user, course_key) if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id: raise Http404 is_staff = has_permission(request.user, 'openclose_thread', course.id) if request.is_ajax(): with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): annotated_content_info = utils.get_annotated_content_infos( course_key, thread, request.user, user_info=user_info ) content = utils.prepare_content(thread.to_dict(), course_key, is_staff) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context([content], course, request.user) return utils.JsonResponse({ 'content': content, 'annotated_content_info': annotated_content_info, }) else: try: threads, query_params = get_threads(request, course) except ValueError: return HttpResponseBadRequest("Invalid group_id") threads.append(thread.to_dict()) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) for thread in threads: # patch for backward compatibility with comments service if "pinned" not in thread: thread["pinned"] = False threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads] with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): user_cohort = get_cohort_id(request.user, course_key) context = { 'discussion_id': discussion_id, 'csrf': csrf(request)['csrf_token'], 'init': '', # TODO: What is this? 'user_info': user_info, 'can_create_comment': has_permission(request.user, "create_comment", course.id), 'can_create_subcomment': has_permission(request.user, "create_sub_comment", course.id), 'can_create_thread': has_permission(request.user, "create_thread", course.id), 'annotated_content_info': annotated_content_info, 'course': course, #'recent_active_threads': recent_active_threads, 'course_id': course.id.to_deprecated_string(), # TODO: Why pass both course and course.id to template? 'thread_id': thread_id, 'threads': threads, 'roles': utils.get_role_ids(course_key), 'is_moderator': is_moderator, 'thread_pages': query_params['num_pages'], 'is_course_cohorted': is_course_cohorted(course_key), 'flag_moderator': bool( has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course) ), 'cohorts': course_settings["cohorts"], 'user_cohort': user_cohort, 'sort_preference': cc_user.default_sort_key, 'category_map': course_settings["category_map"], 'course_settings': course_settings, 'disable_courseware_js': True, 'uses_pattern_library': True, } return render_to_response('discussion/discussion_board.html', context)
def single_thread(request, course_key, discussion_id, thread_id): """ Renders a response to display a single discussion thread. """ # nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, "load", course_key, check_if_enrolled=True) course_settings = make_course_settings(course, request.user) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() is_moderator = has_permission(request.user, "see_all_cohorts", course_key) # Verify that the student has access to this thread if belongs to a discussion module if discussion_id not in utils.get_discussion_categories_ids(course, request.user): raise Http404 # Currently, the front end always loads responses via AJAX, even for this # page; it would be a nice optimization to avoid that extra round trip to # the comments service. try: thread = cc.Thread.find(thread_id).retrieve( recursive=request.is_ajax(), user_id=request.user.id, response_skip=request.GET.get("resp_skip"), response_limit=request.GET.get("resp_limit"), ) except cc.utils.CommentClientRequestError as e: if e.status_code == 404: raise Http404 raise # verify that the thread belongs to the requesting student's cohort if is_commentable_cohorted(course_key, discussion_id) and not is_moderator: user_group_id = get_cohort_id(request.user, course_key) if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id: raise Http404 is_staff = has_permission(request.user, "openclose_thread", course.id) if request.is_ajax(): # with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): annotated_content_info = utils.get_annotated_content_infos( course_key, thread, request.user, user_info=user_info ) content = utils.prepare_content(thread.to_dict(), course_key, is_staff) # with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context([content], course, request.user) return utils.JsonResponse({"content": content, "annotated_content_info": annotated_content_info}) else: try: threads, query_params = get_threads(request, course) except ValueError: return HttpResponseBadRequest("Invalid group_id") threads.append(thread.to_dict()) # with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) for thread in threads: # patch for backward compatibility with comments service if "pinned" not in thread: thread["pinned"] = False threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads] # with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) # with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): user_cohort = get_cohort_id(request.user, course_key) context = { "discussion_id": discussion_id, "csrf": csrf(request)["csrf_token"], "init": "", # TODO: What is this? "user_info": _attr_safe_json(user_info), "annotated_content_info": _attr_safe_json(annotated_content_info), "course": course, #'recent_active_threads': recent_active_threads, "course_id": course.id.to_deprecated_string(), # TODO: Why pass both course and course.id to template? "thread_id": thread_id, "threads": _attr_safe_json(threads), "roles": _attr_safe_json(utils.get_role_ids(course_key)), "is_moderator": is_moderator, "thread_pages": query_params["num_pages"], "is_course_cohorted": is_course_cohorted(course_key), "flag_moderator": ( has_permission(request.user, "openclose_thread", course.id) or has_access(request.user, "staff", course) ), "cohorts": course_settings["cohorts"], "user_cohort": user_cohort, "sort_preference": cc_user.default_sort_key, "category_map": course_settings["category_map"], "course_settings": _attr_safe_json(course_settings), } return render_to_response("discussion/index.html", context)
def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE): """ This may raise an appropriate subclass of cc.utils.CommentClientError if something goes wrong. It may also raise ValueError if the group_id is invalid. """ default_query_params = { 'page': 1, 'per_page': per_page, 'sort_key': 'date', 'sort_order': 'desc', 'text': '', 'course_id': unicode(course.id), 'commentable_id': discussion_id, 'user_id': request.user.id, 'group_id': get_group_id_for_comments_service(request, course.id, discussion_id), # may raise ValueError } # If provided with a discussion id, filter by discussion id in the # comments_service. if discussion_id is not None: default_query_params['commentable_id'] = discussion_id if not request.GET.get('sort_key'): # If the user did not select a sort key, use their last used sort key cc_user = cc.User.from_django_user(request.user) cc_user.retrieve() # TODO: After the comment service is updated this can just be user.default_sort_key because the service returns the default value default_query_params['sort_key'] = cc_user.get('default_sort_key') or default_query_params['sort_key'] else: # If the user clicked a sort key, update their default sort key cc_user = cc.User.from_django_user(request.user) cc_user.default_sort_key = request.GET.get('sort_key') cc_user.save() #there are 2 dimensions to consider when executing a search with respect to group id #is user a moderator #did the user request a group #if the user requested a group explicitly, give them that group, otherwise, if mod, show all, else if student, use cohort if discussion_id: is_cohorted = is_commentable_cohorted(course.id, discussion_id) else: is_cohorted = is_course_cohorted(course.id) if has_permission(request.user, "see_all_cohorts", course.id): group_id = request.GET.get('group_id') if group_id in ("all", "None"): group_id = None else: group_id = get_cohort_id(request.user, course.id) if not group_id: default_query_params['exclude_groups'] = True if group_id: group_id = int(group_id) try: CourseUserGroup.objects.get(course_id=course.id, id=group_id) except CourseUserGroup.DoesNotExist: if not is_cohorted: group_id = None else: raise ValueError("Invalid Group ID") default_query_params["group_id"] = group_id #so by default, a moderator sees all items, and a student sees his cohort query_params = merge_dict( default_query_params, strip_none( extract( request.GET, [ 'page', 'sort_key', 'sort_order', 'text', 'commentable_ids', 'flagged', 'unread', 'unanswered', ] ) ) ) if not is_cohorted: query_params.pop('group_id', None) threads, page, num_pages, corrected_text = cc.Thread.search(query_params) threads = _set_group_names(course.id, threads) query_params['page'] = page query_params['num_pages'] = num_pages query_params['corrected_text'] = corrected_text return threads, query_params
def prepare_content(content, course_key, is_staff=False, course_is_cohorted=None): """ This function is used to pre-process thread and comment models in various ways before adding them to the HTTP response. This includes fixing empty attribute fields, enforcing author anonymity, and enriching metadata around group ownership and response endorsement. @TODO: not all response pre-processing steps are currently integrated into this function. Arguments: content (dict): A thread or comment. course_key (CourseKey): The course key of the course. is_staff (bool): Whether the user is a staff member. course_is_cohorted (bool): Whether the course is cohorted. """ fields = [ 'id', 'title', 'body', 'course_id', 'anonymous', 'anonymous_to_peers', 'endorsed', 'parent_id', 'thread_id', 'votes', 'closed', 'created_at', 'updated_at', 'depth', 'type', 'commentable_id', 'comments_count', 'at_position_list', 'children', 'highlighted_title', 'highlighted_body', 'courseware_title', 'courseware_url', 'unread_comments_count', 'read', 'group_id', 'group_name', 'pinned', 'abuse_flaggers', 'stats', 'resp_skip', 'resp_limit', 'resp_total', 'thread_type', 'endorsed_responses', 'non_endorsed_responses', 'non_endorsed_resp_total', 'endorsement', 'context' ] if (content.get('anonymous') is False) and ((content.get('anonymous_to_peers') is False) or is_staff): fields += ['username', 'user_id'] content = strip_none(extract(content, fields)) if content.get("endorsement"): endorsement = content["endorsement"] endorser = None if endorsement["user_id"]: try: endorser = User.objects.get(pk=endorsement["user_id"]) except User.DoesNotExist: log.error( "User ID %s in endorsement for comment %s but not in our DB.", content.get('user_id'), content.get('id') ) # Only reveal endorser if requester can see author or if endorser is staff if ( endorser and ("username" in fields or has_permission(endorser, "endorse_comment", course_key)) ): endorsement["username"] = endorser.username else: del endorsement["user_id"] if course_is_cohorted is None: course_is_cohorted = is_course_cohorted(course_key) for child_content_key in ["children", "endorsed_responses", "non_endorsed_responses"]: if child_content_key in content: children = [ prepare_content(child, course_key, is_staff, course_is_cohorted=course_is_cohorted) for child in content[child_content_key] ] content[child_content_key] = children if course_is_cohorted: # Augment the specified thread info to include the group name if a group id is present. if content.get('group_id') is not None: content['group_name'] = get_cohort_by_id(course_key, content.get('group_id')).name else: # Remove any cohort information that might remain if the course had previously been cohorted. content.pop('group_id', None) return content
def prepare_content(content, course_key, is_staff=False, course_is_cohorted=None): """ This function is used to pre-process thread and comment models in various ways before adding them to the HTTP response. This includes fixing empty attribute fields, enforcing author anonymity, and enriching metadata around group ownership and response endorsement. @TODO: not all response pre-processing steps are currently integrated into this function. Arguments: content (dict): A thread or comment. course_key (CourseKey): The course key of the course. is_staff (bool): Whether the user is a staff member. course_is_cohorted (bool): Whether the course is cohorted. """ fields = [ 'id', 'title', 'body', 'course_id', 'anonymous', 'anonymous_to_peers', 'endorsed', 'parent_id', 'thread_id', 'votes', 'closed', 'created_at', 'updated_at', 'depth', 'type', 'commentable_id', 'comments_count', 'at_position_list', 'children', 'highlighted_title', 'highlighted_body', 'courseware_title', 'courseware_url', 'unread_comments_count', 'read', 'group_id', 'group_name', 'pinned', 'abuse_flaggers', 'stats', 'resp_skip', 'resp_limit', 'resp_total', 'thread_type', 'endorsed_responses', 'non_endorsed_responses', 'non_endorsed_resp_total', 'endorsement', ] if (content.get('anonymous') is False) and ((content.get('anonymous_to_peers') is False) or is_staff): fields += ['username', 'user_id'] content = strip_none(extract(content, fields)) if content.get("endorsement"): endorsement = content["endorsement"] endorser = None if endorsement["user_id"]: try: endorser = User.objects.get(pk=endorsement["user_id"]) except User.DoesNotExist: log.error("User ID {0} in endorsement for comment {1} but not in our DB.".format( content.get('user_id'), content.get('id')) ) # Only reveal endorser if requester can see author or if endorser is staff if ( endorser and ("username" in fields or has_permission(endorser, "endorse_comment", course_key)) ): endorsement["username"] = endorser.username else: del endorsement["user_id"] if course_is_cohorted is None: course_is_cohorted = is_course_cohorted(course_key) for child_content_key in ["children", "endorsed_responses", "non_endorsed_responses"]: if child_content_key in content: children = [ prepare_content(child, course_key, is_staff, course_is_cohorted=course_is_cohorted) for child in content[child_content_key] ] content[child_content_key] = children if course_is_cohorted: # Augment the specified thread info to include the group name if a group id is present. if content.get('group_id') is not None: content['group_name'] = get_cohort_by_id(course_key, content.get('group_id')).name else: # Remove any cohort information that might remain if the course had previously been cohorted. content.pop('group_id', None) return content
def forum_form_discussion(request, course_key): """ Renders the main Discussion page, potentially filtered by a search query """ nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) course_settings = make_course_settings(course, request.user) user = cc.User.from_django_user(request.user) user_info = user.to_dict() try: unsafethreads, query_params = get_threads(request, course) # This might process a search query is_staff = has_permission(request.user, 'openclose_thread', course.id) threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads] except cc.utils.CommentClientMaintenanceError: log.warning("Forum is in maintenance mode") return render_to_response('discussion/maintenance.html', { 'disable_courseware_js': True, 'uses_pattern_library': True, }) except ValueError: return HttpResponseBadRequest("Invalid group_id") with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) if request.is_ajax(): return utils.JsonResponse({ 'discussion_data': threads, # TODO: Standardize on 'discussion_data' vs 'threads' 'annotated_content_info': annotated_content_info, 'num_pages': query_params['num_pages'], 'page': query_params['page'], 'corrected_text': query_params['corrected_text'], }) else: with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): user_cohort_id = get_cohort_id(request.user, course_key) context = { 'csrf': csrf(request)['csrf_token'], 'course': course, #'recent_active_threads': recent_active_threads, 'staff_access': bool(has_access(request.user, 'staff', course)), 'threads': threads, 'thread_pages': query_params['num_pages'], 'user_info': user_info, 'can_create_comment': has_permission(request.user, "create_comment", course.id), 'can_create_subcomment': has_permission(request.user, "create_sub_comment", course.id), 'can_create_thread': has_permission(request.user, "create_thread", course.id), 'flag_moderator': bool( has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course) ), 'annotated_content_info': annotated_content_info, 'course_id': course.id.to_deprecated_string(), 'roles': utils.get_role_ids(course_key), 'is_moderator': has_permission(request.user, "see_all_cohorts", course_key), 'cohorts': course_settings["cohorts"], # still needed to render _thread_list_template 'user_cohort': user_cohort_id, # read from container in NewPostView 'is_course_cohorted': is_course_cohorted(course_key), # still needed to render _thread_list_template 'sort_preference': user.default_sort_key, 'category_map': course_settings["category_map"], 'course_settings': course_settings, 'disable_courseware_js': True, 'uses_pattern_library': True, } # print "start rendering.." return render_to_response('discussion/discussion_board.html', context)
def _section_send_email(course, access): """ Provide data for the corresponding bulk email section """ course_key = course.id # Monkey-patch applicable_aside_types to return no asides for the duration of this render with patch.object(course.runtime, 'applicable_aside_types', null_applicable_aside_types): # This HtmlBlock is only being used to generate a nice text editor. html_module = HtmlBlock( course.system, DictFieldData({'data': ''}), ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake'))) fragment = course.system.render(html_module, 'studio_view') fragment = wrap_xblock( 'LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": six.text_type(course_key)}, usage_id_serializer=lambda usage_id: quote_slashes( six.text_type(usage_id)), # Generate a new request_token here at random, because this module isn't connected to any other # xblock rendering. request_token=uuid.uuid1().hex) cohorts = [] if is_course_cohorted(course_key): cohorts = get_course_cohorts(course) course_modes = [] if not VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled( course_key): course_modes = CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False) email_editor = fragment.content section_data = { 'section_key': 'send_email', 'section_display_name': _('Email'), 'access': access, 'send_email': reverse('send_email', kwargs={'course_id': six.text_type(course_key)}), 'editor': email_editor, 'cohorts': cohorts, 'course_modes': course_modes, 'default_cohort_name': DEFAULT_COHORT_NAME, 'list_instructor_tasks_url': reverse('api_instructor:list_instructor_tasks', kwargs={'course_id': six.text_type(course_key)}), 'email_background_tasks_url': reverse('list_background_email_tasks', kwargs={'course_id': six.text_type(course_key)}), 'email_content_history_url': reverse('list_email_content', kwargs={'course_id': six.text_type(course_key)}), } return section_data
def forum_form_discussion(request, course_key): """ Renders the main Discussion page, potentially filtered by a search query """ # nr_transaction = newrelic.agent.current_transaction() course = get_course_with_access(request.user, "load", course_key, check_if_enrolled=True) course_settings = make_course_settings(course, request.user) user = cc.User.from_django_user(request.user) user_info = user.to_dict() try: unsafethreads, query_params = get_threads(request, course) # This might process a search query is_staff = has_permission(request.user, "openclose_thread", course.id) threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads] except cc.utils.CommentClientMaintenanceError: log.warning("Forum is in maintenance mode") return render_to_response("discussion/maintenance.html", {}) except ValueError: return HttpResponseBadRequest("Invalid group_id") # with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) # with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) if request.is_ajax(): return utils.JsonResponse( { "discussion_data": threads, # TODO: Standardize on 'discussion_data' vs 'threads' "annotated_content_info": annotated_content_info, "num_pages": query_params["num_pages"], "page": query_params["page"], "corrected_text": query_params["corrected_text"], } ) else: # with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"): user_cohort_id = get_cohort_id(request.user, course_key) context = { "csrf": csrf(request)["csrf_token"], "course": course, #'recent_active_threads': recent_active_threads, "staff_access": has_access(request.user, "staff", course), "threads": _attr_safe_json(threads), "thread_pages": query_params["num_pages"], "user_info": _attr_safe_json(user_info), "flag_moderator": ( has_permission(request.user, "openclose_thread", course.id) or has_access(request.user, "staff", course) ), "annotated_content_info": _attr_safe_json(annotated_content_info), "course_id": course.id.to_deprecated_string(), "roles": _attr_safe_json(utils.get_role_ids(course_key)), "is_moderator": has_permission(request.user, "see_all_cohorts", course_key), "cohorts": course_settings["cohorts"], # still needed to render _thread_list_template "user_cohort": user_cohort_id, # read from container in NewPostView "is_course_cohorted": is_course_cohorted(course_key), # still needed to render _thread_list_template "sort_preference": user.default_sort_key, "category_map": course_settings["category_map"], "course_settings": _attr_safe_json(course_settings), } # print "start rendering.." return render_to_response("discussion/index.html", context)