Commit d0397762 by Vik Paruchuri

Merge branch 'feature/vik/studio-oe' into feature/vik/advanced-studio

Conflicts:
	cms/djangoapps/contentstore/views.py
parents 67b73242 9f020e22
......@@ -66,10 +66,13 @@ from cms.djangoapps.models.settings.course_metadata import CourseMetadata
log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video', 'advanced']
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video', 'advanced', 'openended']
ADVANCED_COMPONENT_TYPES = {
'openended' : ['combinedopenended', 'peergrading'],
'advanced' : ['advanced']
}
# advanced/beta components that can be enabled for all courses or per-course in the policy file
ADVANCED_COMPONENT_TYPES = []
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'enable_advanced_modules'
......@@ -287,25 +290,31 @@ def edit_unit(request, location):
component_templates = defaultdict(list)
# check if there are any advanced modules specified in the course policy
advanced_component_types = list(ADVANCED_COMPONENT_TYPES)
advanced_component_types = ADVANCED_COMPONENT_TYPES
course_metadata = CourseMetadata.fetch(course.location)
advanced_component_types.extend(course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, []))
course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, {})
if isinstance(course_advanced_keys,dict):
advanced_component_types.update(course_advanced_keys)
else:
log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
for template in templates:
component_template = (
template.display_name,
template.location.url(),
'markdown' in template.metadata,
'empty' in template.metadata
)
if template.location.category in COMPONENT_TYPES:
component_templates[template.location.category].append(component_template)
elif template.location.category in advanced_component_types:
component_templates[ADVANCED_COMPONENT_CATEGORY].append(component_template)
if template.location.category in COMPONENT_TYPES:
#This is a hack to create categories for different xmodules
category = template.location.category
for key in ADVANCED_COMPONENT_TYPES:
if template.location.category in ADVANCED_COMPONENT_TYPES[key]:
category = key
break
# order of component types for display purposes
component_template_types = [type for type in COMPONENT_TYPES if type in component_templates.keys()]
component_templates[template.location.category].append((
template.display_name,
template.location.url(),
'markdown' in template.metadata,
'empty' in template.metadata
))
components = [
component.location.url()
......
......@@ -254,6 +254,14 @@
background: url(../img/html-icon.png) center no-repeat;
}
.large-openended-icon {
display: inline-block;
width: 100px;
height: 60px;
margin-right: 5px;
background: url(../img/large-problem-icon.png) center no-repeat;
}
.large-textbook-icon {
display: inline-block;
width: 100px;
......
......@@ -4,5 +4,5 @@ setup(
name="capa",
version="0.1",
packages=find_packages(exclude=["tests"]),
install_requires=['distribute', 'pyparsing'],
install_requires=['distribute==0.6.34', 'pyparsing==1.5.6'],
)
......@@ -333,6 +333,12 @@ class CapaModule(XModule):
reset_button = False
save_button = False
# If attempts=0 then show just check and reset buttons; this is for survey questions using capa
if self.max_attempts==0:
check_button = False
reset_button = True
save_button = True
# User submitted a problem, and hasn't reset. We don't want
# more submissions.
if self.lcp.done and self.rerandomize == "always":
......@@ -630,11 +636,11 @@ class CapaModule(XModule):
event_info['answers'] = answers
# Too late. Cannot submit
if self.closed():
if self.closed() and not self.max_attempts==0:
event_info['failure'] = 'closed'
self.system.track_function('save_problem_fail', event_info)
return {'success': False,
'error': "Problem is closed"}
'msg': "Problem is closed"}
# Problem submitted. Student should reset before saving
# again.
......@@ -642,13 +648,16 @@ class CapaModule(XModule):
event_info['failure'] = 'done'
self.system.track_function('save_problem_fail', event_info)
return {'success': False,
'error': "Problem needs to be reset prior to save."}
'msg': "Problem needs to be reset prior to save"}
self.lcp.student_answers = answers
# TODO: should this be save_problem_fail? Looks like success to me...
self.system.track_function('save_problem_fail', event_info)
return {'success': True}
self.system.track_function('save_problem_success', event_info)
msg = "Your answers have been saved"
if not self.max_attempts==0:
msg += " but not graded. Hit 'Check' to grade them."
return {'success': True,
'msg': msg}
def reset_problem(self, get):
''' Changes problem state to unfinished -- removes student answers,
......
......@@ -4,9 +4,8 @@ from lxml import etree
from pkg_resources import resource_string
from .editing_module import EditingDescriptor
from xmodule.raw_module import RawDescriptor
from .x_module import XModule
from .xml_module import XmlDescriptor
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
log = logging.getLogger("mitx.courseware")
......@@ -134,7 +133,7 @@ class CombinedOpenEndedModule(XModule):
}
self.child_descriptor = descriptors[version_index](self.system)
self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(definition['xml_string']), self.system)
self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(definition['data']), self.system)
self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor,
instance_state = json.dumps(instance_state), metadata = self.metadata, static_data= static_data)
......@@ -165,11 +164,11 @@ class CombinedOpenEndedModule(XModule):
return self.child_module.display_name
class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
class CombinedOpenEndedDescriptor(RawDescriptor):
"""
Module for adding combined open ended questions
"""
mako_template = "widgets/html-edit.html"
mako_template = "widgets/raw-edit.html"
module_class = CombinedOpenEndedModule
filename_extension = "xml"
......@@ -177,35 +176,3 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
has_score = True
template_dir_name = "combinedopenended"
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor"
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
Pull out the individual tasks, the rubric, and the prompt, and parse
Returns:
{
'rubric': 'some-html',
'prompt': 'some-html',
'task_xml': dictionary of xml strings,
}
"""
return {'xml_string' : etree.tostring(xml_object), 'metadata' : xml_object.attrib}
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('combinedopenended')
def add_child(k):
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
child_node = etree.fromstring(child_str)
elt.append(child_node)
for child in ['task']:
add_child(child)
return elt
......@@ -262,9 +262,8 @@ class @Problem
save: =>
Logger.log 'problem_save', @answers
$.postWithPrefix "#{@url}/problem_save", @answers, (response) =>
if response.success
saveMessage = "Your answers have been saved but not graded. Hit 'Check' to grade them."
@gentle_alert saveMessage
saveMessage = response.msg
@gentle_alert saveMessage
@updateProgress response
refreshMath: (event, element) =>
......
......@@ -79,6 +79,9 @@ class CombinedOpenEndedV1Module():
INTERMEDIATE_DONE = 'intermediate_done'
DONE = 'done'
#Where the templates live for this problem
TEMPLATE_DIR = "combinedopenended"
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, metadata = None, static_data = None, **kwargs):
......@@ -343,7 +346,7 @@ class CombinedOpenEndedV1Module():
Output: rendered html
"""
context = self.get_context()
html = self.system.render_template('combined_open_ended.html', context)
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
return html
def get_html_nonsystem(self):
......@@ -354,7 +357,7 @@ class CombinedOpenEndedV1Module():
Output: HTML rendered directly via Mako
"""
context = self.get_context()
html = self.system.render_template('combined_open_ended.html', context)
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
return html
def get_html_base(self):
......@@ -531,7 +534,7 @@ class CombinedOpenEndedV1Module():
'task_name' : 'Scored Rubric',
'class_name' : 'combined-rubric-container'
}
html = self.system.render_template('combined_open_ended_results.html', context)
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
def get_legend(self, get):
......@@ -543,7 +546,7 @@ class CombinedOpenEndedV1Module():
context = {
'legend_list' : LEGEND_LIST,
}
html = self.system.render_template('combined_open_ended_legend.html', context)
html = self.system.render_template('{0}/combined_open_ended_legend.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
def get_results(self, get):
......@@ -574,7 +577,7 @@ class CombinedOpenEndedV1Module():
'submission_id' : ri['submission_ids'][i],
}
context_list.append(context)
feedback_table = self.system.render_template('open_ended_result_table.html', {
feedback_table = self.system.render_template('{0}/open_ended_result_table.html'.format(self.TEMPLATE_DIR), {
'context_list' : context_list,
'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT,
'human_grader_types' : HUMAN_GRADER_TYPE,
......@@ -586,7 +589,7 @@ class CombinedOpenEndedV1Module():
'task_name' : "Feedback",
'class_name' : "result-container",
}
html = self.system.render_template('combined_open_ended_results.html', context)
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
return {'html': html, 'success': True}
def get_status_ajax(self, get):
......@@ -700,7 +703,7 @@ class CombinedOpenEndedV1Module():
'legend_list' : LEGEND_LIST,
'render_via_ajax' : render_via_ajax,
}
status_html = self.system.render_template("combined_open_ended_status.html", context)
status_html = self.system.render_template("{0}/combined_open_ended_status.html".format(self.TEMPLATE_DIR), context)
return status_html
......
......@@ -30,6 +30,8 @@ class RubricParsingError(Exception):
class CombinedOpenEndedRubric(object):
TEMPLATE_DIR = "combinedopenended/openended"
def __init__ (self, system, view_only = False):
self.has_score = False
self.view_only = view_only
......@@ -57,9 +59,9 @@ class CombinedOpenEndedRubric(object):
rubric_scores = [cat['score'] for cat in rubric_categories]
max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
max_score = max(max_scores)
rubric_template = 'open_ended_rubric.html'
rubric_template = '{0}/open_ended_rubric.html'.format(self.TEMPLATE_DIR)
if self.view_only:
rubric_template = 'open_ended_view_only_rubric.html'
rubric_template = '{0}/open_ended_view_only_rubric.html'.format(self.TEMPLATE_DIR)
html = self.system.render_template(rubric_template,
{'categories': rubric_categories,
'has_score': self.has_score,
......@@ -207,7 +209,7 @@ class CombinedOpenEndedRubric(object):
for grader_type in tuple[3]:
rubric_categories[i]['options'][j]['grader_types'].append(grader_type)
html = self.system.render_template('open_ended_combined_rubric.html',
html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR),
{'categories': rubric_categories,
'has_score': True,
'view_only': True,
......
......@@ -40,6 +40,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
</openended>
"""
TEMPLATE_DIR = "combinedopenended/openended"
def setup_response(self, system, location, definition, descriptor):
"""
Sets up the response type.
......@@ -397,10 +399,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_scores = rubric_dict['rubric_scores']
if not response_items['success']:
return system.render_template("open_ended_error.html",
return system.render_template("{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
{'errors': feedback})
feedback_template = system.render_template("open_ended_feedback.html", {
feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), {
'grader_type': response_items['grader_type'],
'score': "{0} / {1}".format(response_items['score'], self.max_score()),
'feedback': feedback,
......@@ -558,7 +560,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
@return: Rendered html
"""
context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50}
html = system.render_template('open_ended_evaluation.html', context)
html = system.render_template('{0}/open_ended_evaluation.html'.format(self.TEMPLATE_DIR), context)
return html
def handle_ajax(self, dispatch, get, system):
......@@ -692,7 +694,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'accept_file_upload': self.accept_file_upload,
'eta_message' : eta_string,
}
html = system.render_template('open_ended.html', context)
html = system.render_template('{0}/open_ended.html'.format(self.TEMPLATE_DIR), context)
return html
......
......@@ -22,7 +22,7 @@ from xmodule.stringify import stringify_children
from xmodule.xml_module import XmlDescriptor
from xmodule.modulestore import Location
from capa.util import *
from peer_grading_service import PeerGradingService
from peer_grading_service import PeerGradingService, MockPeerGradingService
import controller_query_service
from datetime import datetime
......@@ -106,8 +106,14 @@ class OpenEndedChild(object):
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = static_data['max_score']
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,system)
if system.open_ended_grading_interface:
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,system)
else:
self.peer_gs = MockPeerGradingService()
self.controller_qs = None
self.system = system
......@@ -461,11 +467,14 @@ class OpenEndedChild(object):
return success, allowed_to_submit, error_message
def get_eta(self):
response = self.controller_qs.check_for_eta(self.location_string)
try:
response = json.loads(response)
except:
pass
if self.controller_qs:
response = self.controller_qs.check_for_eta(self.location_string)
try:
response = json.loads(response)
except:
pass
else:
return ""
success = response['success']
if isinstance(success, basestring):
......
......@@ -32,6 +32,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
</selfassessment>
"""
TEMPLATE_DIR = "combinedopenended/selfassessment"
def setup_response(self, system, location, definition, descriptor):
"""
Sets up the module
......@@ -68,7 +70,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
'accept_file_upload': self.accept_file_upload,
}
html = system.render_template('self_assessment_prompt.html', context)
html = system.render_template('{0}/self_assessment_prompt.html'.format(self.TEMPLATE_DIR), context)
return html
......@@ -129,7 +131,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
#This is a dev_facing_error
raise ValueError("Self assessment module is in an illegal state '{0}'".format(self.state))
return system.render_template('self_assessment_rubric.html', context)
return system.render_template('{0}/self_assessment_rubric.html'.format(self.TEMPLATE_DIR), context)
def get_hint_html(self, system):
"""
......@@ -155,7 +157,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
#This is a dev_facing_error
raise ValueError("Self Assessment module is in an illegal state '{0}'".format(self.state))
return system.render_template('self_assessment_hint.html', context)
return system.render_template('{0}/self_assessment_hint.html'.format(self.TEMPLATE_DIR), context)
def save_answer(self, get, system):
......
......@@ -14,7 +14,7 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from timeinfo import TimeInfo
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
log = logging.getLogger(__name__)
......@@ -53,13 +53,28 @@ class PeerGradingModule(XModule):
#We need to set the location here so the child modules can use it
system.set('location', location)
self.system = system
self.peer_gs = PeerGradingService(self.system.open_ended_grading_interface, self.system)
if(self.system.open_ended_grading_interface):
self.peer_gs = PeerGradingService(self.system.open_ended_grading_interface, self.system)
else:
self.peer_gs = MockPeerGradingService()
self.use_for_single_location = self.metadata.get('use_for_single_location', USE_FOR_SINGLE_LOCATION)
if isinstance(self.use_for_single_location, basestring):
self.use_for_single_location = (self.use_for_single_location in TRUE_DICT)
self.link_to_location = self.metadata.get('link_to_location', USE_FOR_SINGLE_LOCATION)
if self.use_for_single_location == True:
try:
self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
except:
log.error("Linked location {0} for peer grading module {1} does not exist".format(
self.link_to_location, self.location))
raise
due_date = self.linked_problem.metadata.get('peer_grading_due', None)
if due_date:
self.metadata['due'] = due_date
self.is_graded = self.metadata.get('is_graded', IS_GRADED)
if isinstance(self.is_graded, basestring):
self.is_graded = (self.is_graded in TRUE_DICT)
......@@ -75,17 +90,6 @@ class PeerGradingModule(XModule):
self.display_due_date = self.timeinfo.display_due_date
self.link_to_location = self.metadata.get('link_to_location', USE_FOR_SINGLE_LOCATION)
if self.use_for_single_location == True:
try:
self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
except:
log.error("Linked location {0} for peer grading module {1} does not exist".format(
self.link_to_location, self.location))
raise
due_date = self.linked_problem.metadata.get('peer_grading_due', None)
if due_date:
self.metadata['due'] = due_date
self.ajax_url = self.system.ajax_url
if not self.ajax_url.endswith("/"):
......@@ -452,6 +456,7 @@ class PeerGradingModule(XModule):
try:
problem_list_json = self.peer_gs.get_problem_list(self.system.course_id, self.system.anonymous_student_id)
problem_list_dict = problem_list_json
log.debug(problem_list_dict)
success = problem_list_dict['success']
if 'error' in problem_list_dict:
error_text = problem_list_dict['error']
......@@ -467,6 +472,9 @@ class PeerGradingModule(XModule):
#This is a student_facing_error
error_text = "Could not get list of problems to peer grade. Please notify course staff."
success = False
except:
log.exception("Could not contact peer grading service.")
success = False
def _find_corresponding_module_for_location(location):
......@@ -562,7 +570,7 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
"""
Module for adding combined open ended questions
"""
mako_template = "widgets/html-edit.html"
mako_template = "widgets/raw-edit.html"
module_class = PeerGradingModule
filename_extension = "xml"
......@@ -606,13 +614,4 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('peergrading')
def add_child(k):
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
child_node = etree.fromstring(child_str)
elt.append(child_node)
for child in ['task']:
add_child(child)
return elt
---
metadata:
display_name: Open Ended Response
rerandomize: never
showanswer: never
weight: ""
attempts: 10
max_score: 1
is_graded: False
version: 1
display_name: Test Question
data: |
<combinedopenended attempts="10" max_score="1" display_name = "Open Ended Response" is_graded="True" version="1">
<rubric>
<rubric>
<category>
<description>Category 1</description>
<option>
The response does not incorporate what is needed for a one response.
</option>
<option>
The response is correct for category 1.
</option>
</category>
</rubric>
</rubric>
<prompt>
<p>Why is the sky blue?</p>
</prompt>
<task>
<selfassessment/>
</task>
<task>
<openended min_score_to_attempt="1" max_score_to_attempt="2">
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "peer_grading.conf", "problem_id" : "700x/Demo"}</grader_payload>
</openendedparam>
</openended>
</task>
</combinedopenended>
children: []
---
metadata:
display_name: Peer Grading Interface
rerandomize: never
showanswer: never
weight: ""
attempts: ""
data: |
<peergrading use_for_single_location="False" is_graded="False" display_name = "Peer Grading Panel">
</peergrading>
children: []
"""This file contains (or should), all access control logic for the courseware.
Ideally, it will be the only place that needs to know about any special settings
like DISABLE_START_DATES"""
import logging
import time
from datetime import datetime, timedelta
from functools import partial
from django.conf import settings
from django.contrib.auth.models import Group
......@@ -363,21 +363,15 @@ def _course_org_staff_group_name(location, course_context=None):
return 'staff_%s' % course_id.split('/')[0]
def _course_staff_group_name(location, course_context=None):
"""
Get the name of the staff group for a location in the context of a course run.
location: something that can passed to Location
course_context: A course_id that specifies the course run in which the location occurs.
Required if location doesn't have category 'course'
cdodge: We're changing the name convention of the group to better epxress different runs of courses by
using course_id rather than just the course number. So first check to see if the group name exists
"""
def group_names_for(role, location, course_context=None):
"""Returns the group names for a given role with this location. Plural
because it will return both the name we expect now as well as the legacy
group name we support for backwards compatibility. This should not check
the DB for existence of a group (like some of its callers do) because that's
a DB roundtrip, and we expect this might be invoked many times as we crawl
an XModule tree."""
loc = Location(location)
legacy_name = 'staff_%s' % loc.course
if _does_course_group_name_exist(legacy_name):
return legacy_name
legacy_group_name = '{0}_{1}'.format(role, loc.course)
if loc.category == 'course':
course_id = loc.course_id
......@@ -386,22 +380,31 @@ def _course_staff_group_name(location, course_context=None):
raise CourseContextRequired()
course_id = course_context
return 'staff_%s' % course_id
group_name = '{0}_{1}'.format(role, course_id)
return [group_name, legacy_group_name]
def course_beta_test_group_name(location):
group_names_for_staff = partial(group_names_for, 'staff')
group_names_for_instructor = partial(group_names_for, 'instructor')
def _course_staff_group_name(location, course_context=None):
"""
Get the name of the beta tester group for a location. Right now, that's
beta_testers_COURSE.
Get the name of the staff group for a location in the context of a course run.
location: something that can passed to Location.
location: something that can passed to Location
course_context: A course_id that specifies the course run in which the location occurs.
Required if location doesn't have category 'course'
cdodge: We're changing the name convention of the group to better epxress different runs of courses by
using course_id rather than just the course number. So first check to see if the group name exists
"""
return 'beta_testers_{0}'.format(Location(location).course)
loc = Location(location)
group_name, legacy_group_name = group_names_for_staff(location, course_context)
# nosetests thinks that anything with _test_ in the name is a test.
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
course_beta_test_group_name.__test__ = False
if _does_course_group_name_exist(legacy_group_name):
return legacy_group_name
return group_name
def _course_org_instructor_group_name(location, course_context=None):
"""
......@@ -437,18 +440,26 @@ def _course_instructor_group_name(location, course_context=None):
using course_id rather than just the course number. So first check to see if the group name exists
"""
loc = Location(location)
legacy_name = 'instructor_%s' % loc.course
if _does_course_group_name_exist(legacy_name):
return legacy_name
group_name, legacy_group_name = group_names_for_instructor(location, course_context)
if loc.category == 'course':
course_id = loc.course_id
else:
if course_context is None:
raise CourseContextRequired()
course_id = course_context
if _does_course_group_name_exist(legacy_group_name):
return legacy_group_name
return group_name
def course_beta_test_group_name(location):
"""
Get the name of the beta tester group for a location. Right now, that's
beta_testers_COURSE.
location: something that can passed to Location.
"""
return 'beta_testers_{0}'.format(Location(location).course)
# nosetests thinks that anything with _test_ in the name is a test.
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
course_beta_test_group_name.__test__ = False
return 'instructor_%s' % course_id
def _has_global_staff_access(user):
......@@ -540,23 +551,22 @@ def _has_access_to_location(user, location, access_level, course_context):
user_groups = [g.name for g in user.groups.all()]
if access_level == 'staff':
staff_group = _course_staff_group_name(location, course_context)
# org_staff_group is a group for an entire organization
org_staff_group = _course_org_staff_group_name(location, course_context)
if staff_group in user_groups or org_staff_group in user_groups:
debug("Allow: user in group %s", staff_group)
return True
debug("Deny: user not in group %s", staff_group)
staff_groups = group_names_for_staff(location, course_context) + \
[_course_org_staff_group_name(location, course_context)]
for staff_group in staff_groups:
if staff_group in user_groups:
debug("Allow: user in group %s", staff_group)
return True
debug("Deny: user not in groups %s", staff_groups)
if access_level == 'instructor' or access_level == 'staff': # instructors get staff privileges
instructor_group = _course_instructor_group_name(location, course_context)
instructor_staff_group = _course_org_instructor_group_name(
location, course_context)
if instructor_group in user_groups or instructor_staff_group in user_groups:
debug("Allow: user in group %s", instructor_group)
return True
debug("Deny: user not in group %s", instructor_group)
instructor_groups = group_names_for_instructor(location, course_context) + \
[_course_org_instructor_group_name(location, course_context)]
for instructor_group in instructor_groups:
if instructor_group in user_groups:
debug("Allow: user in group %s", instructor_group)
return True
debug("Deny: user not in groups %s", instructor_groups)
else:
log.debug("Error in access._has_access_to_location access_level=%s unknown" % access_level)
......
......@@ -86,7 +86,8 @@ def render_accordion(request, course, chapter, section):
Returns the html string'''
# grab the table of contents
toc = toc_for_course(request.user, request, course, chapter, section)
user = User.objects.prefetch_related("groups").get(id=request.user.id)
toc = toc_for_course(user, request, course, chapter, section)
context = dict([('toc', toc),
('course_id', course.id),
......@@ -250,23 +251,24 @@ def index(request, course_id, chapter=None, section=None,
- HTTPresponse
"""
course = get_course_with_access(request.user, course_id, 'load', depth=2)
staff_access = has_access(request.user, course, 'staff')
registered = registered_for_course(course, request.user)
user = User.objects.prefetch_related("groups").get(id=request.user.id)
course = get_course_with_access(user, course_id, 'load', depth=2)
staff_access = has_access(user, course, 'staff')
registered = registered_for_course(course, user)
if not registered:
# TODO (vshnayder): do course instructors need to be registered to see course?
log.debug('User %s tried to view course %s but is not enrolled' % (request.user, course.location.url()))
log.debug('User %s tried to view course %s but is not enrolled' % (user, course.location.url()))
return redirect(reverse('about_course', args=[course.id]))
try:
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
course.id, request.user, course, depth=2)
course.id, user, course, depth=2)
# Has this student been in this course before?
first_time = student_module_cache.lookup(course_id, 'course', course.location.url()) is None
# Load the module for the course
course_module = get_module_for_descriptor(request.user, request, course, student_module_cache, course.id)
course_module = get_module_for_descriptor(user, request, course, student_module_cache, course.id)
if course_module is None:
log.warning('If you see this, something went wrong: if we got this'
' far, should have gotten a course module for this user')
......@@ -288,7 +290,7 @@ def index(request, course_id, chapter=None, section=None,
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
if chapter_descriptor is not None:
instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
instance_module = get_instance_module(course_id, user, course_module, student_module_cache)
save_child_position(course_module, chapter, instance_module)
else:
raise Http404('No chapter descriptor found with name {}'.format(chapter))
......@@ -307,9 +309,9 @@ def index(request, course_id, chapter=None, section=None,
# Load all descendants of the section, because we're going to display its
# html, which in general will need all of its children
section_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
course.id, request.user, section_descriptor, depth=None)
course.id, user, section_descriptor, depth=None)
section_module = get_module(request.user, request, section_descriptor.location,
section_module = get_module(user, request, section_descriptor.location,
section_module_cache, course.id, position=position, depth=None)
if section_module is None:
# User may be trying to be clever and access something
......@@ -317,12 +319,12 @@ def index(request, course_id, chapter=None, section=None,
raise Http404
# Save where we are in the chapter
instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
instance_module = get_instance_module(course_id, user, chapter_module, student_module_cache)
save_child_position(chapter_module, section, instance_module)
# check here if this section *is* a timed module.
if section_module.category == 'timelimit':
timer_context = update_timelimit_module(request.user, course_id, student_module_cache,
timer_context = update_timelimit_module(user, course_id, student_module_cache,
section_descriptor, section_module)
if 'timer_expiration_duration' in timer_context:
context.update(timer_context)
......@@ -363,7 +365,7 @@ def index(request, course_id, chapter=None, section=None,
log.exception("Error in index view: user={user}, course={course},"
" chapter={chapter} section={section}"
"position={position}".format(
user=request.user,
user=user,
course=course,
chapter=chapter,
section=section,
......
# Create your views here.
import json
from datetime import datetime
from django.http import HttpResponse, Http404
from django.http import Http404
from mitxmako.shortcuts import render_to_response
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from django.contrib.auth.models import User
def dictfetchall(cursor):
'''Returns all rows from a cursor as a dict.
'''Returns a list of all rows from a cursor as a column: result dict.
Borrowed from Django documentation'''
desc = cursor.description
return [
dict(zip([col[0] for col in desc], row))
for row in cursor.fetchall()
]
table=[]
table.append([col[0] for col in desc])
table = table + cursor.fetchall()
print "Table: " + str(table)
return table
def SQL_query_to_list(cursor, query_string):
cursor.execute(query_string)
raw_result=dictfetchall(cursor)
print raw_result
return raw_result
def dashboard(request):
"""
Quick hack to show staff enrollment numbers. This should be
replaced with a real dashboard later. This version is a short-term
bandaid for the next couple weeks.
Slightly less hackish hack to show staff enrollment numbers and other
simple queries.
All queries here should be indexed and simple. Mostly, this means don't
touch courseware_studentmodule, as tempting as it may be.
"""
if not request.user.is_staff:
raise Http404
queries = []
queries.append("select count(user_id) as students, course_id from student_courseenrollment group by course_id order by students desc;")
queries.append("select count(distinct user_id) as unique_students from student_courseenrollment;")
queries.append("select registrations, count(registrations) from (select count(user_id) as registrations from student_courseenrollment group by user_id) as registrations_per_user group by registrations;")
# results are passed to the template. The template knows how to render
# two types of results: scalars and tables. Scalars should be represented
# as "Visible Title": Value and tables should be lists of lists where each
# inner list represents a single row of the table
results = {"scalars":{},"tables":{}}
# count how many users we have
results["scalars"]["Unique Usernames"]=User.objects.filter().count()
results["scalars"]["Activated Usernames"]=User.objects.filter(is_active=1).count()
# count how many enrollments we have
results["scalars"]["Total Enrollments Across All Courses"]=CourseEnrollment.objects.count()
# establish a direct connection to the database (for executing raw SQL)
from django.db import connection
cursor = connection.cursor()
results = []
for query in queries:
cursor.execute(query)
results.append(dictfetchall(cursor))
# define the queries that will generate our user-facing tables
# table queries need not take the form of raw SQL, but do in this case since
# the MySQL backend for django isn't very friendly with group by or distinct
table_queries = {}
table_queries["course enrollments"]= \
"select "+ \
"course_id as Course, "+ \
"count(user_id) as Students " + \
"from student_courseenrollment "+ \
"group by course_id "+ \
"order by students desc;"
table_queries["number of students in each number of classes"]= \
"select registrations as 'Registered for __ Classes' , "+ \
"count(registrations) as Users "+ \
"from (select count(user_id) as registrations "+ \
"from student_courseenrollment "+ \
"group by user_id) as registrations_per_user "+ \
"group by registrations;"
# add the result for each of the table_queries to the results object
for query in table_queries.keys():
cursor.execute(table_queries[query])
results["tables"][query] = SQL_query_to_list(cursor, table_queries[query])
context={"results":results}
return HttpResponse(json.dumps(results, indent=4))
return render_to_response("admin_dashboard.html",context)
<%namespace name='static' file='static_content.html'/>
<%inherit file="main.html" />
<section class="container about">
<section class="basic_stats">
<div class="edx_summary">
<h2>edX-wide Summary</h2>
<table style="margin-left:auto;margin-right:auto;width:50%">
% for key in results["scalars"]:
<tr>
<td>${key}</td>
<td>${results["scalars"][key]}</td>
</tr>
% endfor
</table>
</div>
% for table in results["tables"]:
<br/>
<div class="table_display">
<h2>${table}</h2>
<table style="margin-left:auto;margin-right:auto;width:50%">
<tr>
% for column in results["tables"][table][0]:
<td><b>${column}</b></td>
% endfor
</tr>
% for row in results["tables"][table][1:]:
<tr>
% for column in row:
<td>${column}</td>
% endfor
</tr>
% endfor
</table>
</div>
% endfor
</section>
</section>
......@@ -20,7 +20,7 @@ http://sympy.googlecode.com/files/sympy-0.7.1.tar.gz
newrelic==1.8.0.13
glob2==0.3
pymongo==2.4.1
django_nose
django_nose==1.1
nosexcover==1.0.7
rednose==0.3.3
GitPython==0.3.2.RC1
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment