Commit 0797f184 by Sef Kloninger

remove queryable_student_module

Even though not running in Stanford production, was causing tests to
fail.  We can resurrect sometime later if/when it's useful.
parent 7531afdd
"""
queryable app allows 2 commands to be run from rake:
populate_studentgrade
populate_studentmoduleexpand
These commands populate table in the SQL database and allow for the class_dashboard app
to render course metrics under the instructor dashboard.
"""
"""
Commands for queryable_student_module app:
populate_studentgrades
populate_studentmoduleexpand
"""
"""
Commands for queryable app:
populate_studentgrades
populate_studentmoduleexpand
"""
"""
======== Populate Student Grades ====================================================================================
Populates the student grade tables of the queryable_table model (CourseGrade, AssignmentTypeGrade, AssignmentGrade).
For the provided course_id, it will find all students that may have changed their grade since the last populate. Of
these students rows for the course grade and assignment type are created only if the student has submitted at
least one answer to any problem in the course. Rows for assignments are only created if the student has submitted an
answer to one of the problems in that assignment. Updates only occur if there is a change in the values the row should
be storing.
"""
import re
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from courseware import grades
from courseware.courses import get_course_by_id
from courseware.models import StudentModule
from queryable_student_module.models import Log, CourseGrade, AssignmentTypeGrade, AssignmentGrade
from queryable_student_module import util
################## Helper Functions ##################
def update_course_grade(course_grade, gradeset):
"""
Returns true if the course grade needs to be updated.
"""
return (not util.approx_equal(course_grade.percent, gradeset['percent'])) or (course_grade.grade != gradeset['grade'])
def get_assignment_index(assignment):
"""
Returns the assignment's index, -1 if an index can't be found.
`assignment` is a string formatted like this "HW 02" and this function returns 2 in this case.
The string is the 'label' for each section in the 'section_breakdown' of the dictionary returned by the grades.grade
function.
"""
match = re.search(r'.* (\d+)', assignment)
index = -1
if match:
index = int(match.group(1)) - 1
return index
def assignment_exists_and_has_prob(assignment_problems_map, category, index):
"""
Returns True if the assignment for the category and index exists and has problems
`assignment_problems_map` a dictionary returned by get_assignment_to_problem_map(course_id)
`category` string specifying the category or assignment type for this assignment
`index` zero-based indexing into the array of assignments for that category
"""
if index < 0:
return False
if category not in assignment_problems_map:
return False
if index >= len(assignment_problems_map[category]):
return False
return len(assignment_problems_map[category][index]) > 0
def get_student_problems(course_id, student):
"""
Returns an array of problem ids that the student has answered for this course.
`course_id` the course ID for the course interested in
`student` the student want to get his/her problems
Queries the database to get the problems the student has submitted an answer to for the course specified.
"""
query = StudentModule.objects.filter(
course_id__exact=course_id,
student=student,
grade__isnull=False,
module_type__exact='problem',
).values('module_state_key').distinct()
student_problems = []
for problem in query:
student_problems.append(problem['module_state_key'])
return student_problems
def student_did_problems(student_problems, problem_set):
"""
Returns true if `student_problems` and `problem_set` share problems.
`student_problems` array of problem ids the student has done
`problem_set` array of problem ids
"""
return (set(student_problems) & set(problem_set))
def store_course_grade_if_need(student, course_id, gradeset):
"""
Stores the course grade for the student and course if needed, returns True if it was stored
`student` is a User object representing the student
`course_id` the course's ID
`gradeset` the values returned by grades.grade
The course grade is stored if it has never been stored before (i.e. this is a new row in the database) or
update_course_grade is true.
"""
course_grade, created = CourseGrade.objects.get_or_create(user_id=student.id,
course_id=course_id,
defaults={'username': student.username,
'name': student.profile.name})
if created or update_course_grade(course_grade, gradeset):
course_grade.percent = gradeset['percent']
course_grade.grade = gradeset['grade']
course_grade.save()
return True
return False
def store_assignment_type_grade(student, course_id, category, percent):
"""
Stores the assignment type grade for the student and course if needed, returns True if it was stored
`student` is a User object representing the student
`course_id` the course's ID
`category` the category for the assignment type, found in the return value of the grades.grade function
`percent` the percent grade the student received for this assignment
The assignment type grade is stored if it has never been stored before (i.e. this is a new row in the database) or
if the percent value is different than what is currently in the database.
"""
assign_type_grade, created = AssignmentTypeGrade.objects.get_or_create(
user_id=student.id,
course_id=course_id,
category=category,
defaults={'username': student.username,
'name': student.profile.name})
if created or not util.approx_equal(assign_type_grade.percent, percent):
assign_type_grade.percent = percent
assign_type_grade.save()
return True
return False
def store_assignment_grade_if_need(student, course_id, label, percent):
"""
Stores the assignment grade for the student and course if needed, returns True if it was stored
`student` is a User object representing the student
`course_id` the course's ID
`label` the label for the assignment, found in the return value of the grades.grade function
`percent` the percent grade the student received for this assignment
The assignment grade is stored if it has never been stored before (i.e. this is a new row in the database) or
if the percent value is different than what is currently in the database.
"""
assign_grade, created = AssignmentGrade.objects.get_or_create(
user_id=student.id,
course_id=course_id,
label=label,
defaults={'username':student.username,
'name':student.profile.name})
if created or not util.approx_equal(assign_grade.percent, percent):
assign_grade.percent = percent
assign_grade.save()
return True
return False
class Command(BaseCommand):
"""
populate_studentgrades command
"""
help = "Populates the queryable.StudentGrades table.\n"
help += "Usage: populate_studentgrades course_id\n"
help += " course_id: course's ID, such as Medicine/HRP258/Statistics_in_Medicine\n"
option_list = BaseCommand.option_list + (util.more_options(),)
def handle(self, *args, **options):
script_id = "studentgrades"
print "args = ", args
if len(args) > 0:
course_id = args[0]
else:
print self.help
return
assignment_problems_map = util.get_assignment_to_problem_map(course_id)
iterative_populate, tstart, last_log_run = util.pre_run_command(script_id, options, course_id)
# If iterative populate get all students since last populate, otherwise get all students that fit the criteria.
# Criteria: match course_id, module_type is 'problem', grade is not null because it means they have submitted an
# answer to a problem that might effect their grade.
if iterative_populate:
students = User.objects.select_related('profile').filter(studentmodule__course_id=course_id,
studentmodule__module_type='problem',
studentmodule__grade__isnull=False,
studentmodule__modified__gte=last_log_run[0].created).distinct()
else:
students = User.objects.select_related('profile').filter(studentmodule__course_id=course_id,
studentmodule__module_type='problem',
studentmodule__grade__isnull=False).distinct()
#.select_related('profile')
# Create a dummy request to pass to the grade function.
# Code originally from lms/djangoapps/instructor/offline_gradecalc.py
# Copying instead of using that code so everything is self contained in this django app.
class DummyRequest(object):
"""
Create a dummy request to pass to the grade function.
Code originally from lms/djangoapps/instructor/offline_gradecalc.py
Copying instead of using that code so everything is self contained in this django app.
"""
META = {}
def __init__(self):
self.user = None
return
def is_secure(self):
return False
def get_host(self):
return 'edx.mit.edu'
# Get course using the id, to pass to the grade function
course = get_course_by_id(course_id)
c_updated_students = 0
for student in students:
updated = False
student_problems = None
# Create dummy request and set its user
request = DummyRequest()
request.user = student
request.session = {}
# Call grade to get the gradeset
gradeset = grades.grade(student, request, course, keep_raw_scores=False)
updated = store_course_grade_if_need(student, course_id, gradeset)
# Iterate through the section_breakdown
for section in gradeset['section_breakdown']:
# If the dict has 'prominent' and it's True this is at the assignment type level, store it if need
if ('prominent' in section) and section['prominent']:
updated = store_assignment_type_grade(
student, course_id, section['category'], section['percent']
)
else: # If no 'prominent' or it's False this is at the assignment level
store = False
# If the percent is 0, there are three possibilities:
# 1. There are no problems for that assignment yet -> skip section
# 2. The student hasn't submitted an answer to any problem for that assignment -> skip section
# 3. The student has submitted answers and got zero -> record
# Only store for #3
if section['percent'] > 0:
store = True
else:
# Find which assignment this is for this type/category
index = get_assignment_index(section['label'])
if index < 0:
print "WARNING: Can't find index for the following section, skipping"
print section
else:
if assignment_exists_and_has_prob(assignment_problems_map, section['category'], index):
# Get problems student has done, only do this database call if needed
if student_problems is None:
student_problems = get_student_problems(course_id, student)
curr_assignment_problems = assignment_problems_map[section['category']][index]
if student_did_problems(student_problems, curr_assignment_problems):
store = True
if store:
updated = store_assignment_grade_if_need(
student, course_id, section['label'], section['percent']
)
if updated:
c_updated_students += 1
c_all_students = len(students)
print "--------------------------------------------------------------------------------"
print "Done! Updated {0} students' grades out of {1}".format(c_updated_students, c_all_students)
print "--------------------------------------------------------------------------------"
# Save since everything finished successfully, log latest run.
q_log = Log(script_id=script_id, course_id=course_id, created=tstart)
q_log.save()
"""
======== Populate StudentModuleExpand ===============================================================================
Populates the StudentModuleExpand table of the queryable_table model.
For the provided course_id, it will find all rows in the StudentModule table of the courseware model that have
module_type 'problem' and the grade is not null. Then for any rows that have changed since the last populate or do not
have a corresponding row, update the attempts value.
"""
import json
from django.core.management.base import BaseCommand
from courseware.models import StudentModule
from queryable_student_module.models import Log, StudentModuleExpand
from queryable_student_module.util import pre_run_command, more_options
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
class Command(BaseCommand):
"""
populate_studentmoduleexpand command
"""
help = "Populates the queryable.StudentModuleExpand table.\n"
help += "Usage: populate_studentmoduleexpand course_id\n"
help += " course_id: course's ID, such as Medicine/HRP258/Statistics_in_Medicine\n"
option_list = BaseCommand.option_list + (more_options(),)
def handle(self, *args, **options):
script_id = "studentmoduleexpand"
print "args = ", args
if len(args) > 0:
course_id = args[0]
else:
print self.help
return
iterative_populate, tstart, last_log_run = pre_run_command(script_id, options, course_id)
# If iterative populate, get all the problems that students have submitted an answer to for this course,
# since the last run
if iterative_populate:
sm_rows = StudentModule.objects.select_related('student', 'student__profile').filter(course_id__exact=course_id, grade__isnull=False,
module_type__exact="problem", modified__gte=last_log_run[0].created)
else:
sm_rows = StudentModule.objects.select_related('student', 'student__profile').filter(course_id__exact=course_id, grade__isnull=False,
module_type__exact="problem")
c_updated_rows = 0
# For each problem, get or create the corresponding StudentModuleExpand row
for sm_row in sm_rows:
# Get the display name for the problem
module_state_key = sm_row.module_state_key
problem = modulestore().get_instance(course_id, module_state_key, 0)
problem_name = own_metadata(problem)['display_name']
sme, created = StudentModuleExpand.objects.get_or_create(student_id=sm_row.student_id, course_id=course_id,
module_state_key=module_state_key,
student_module_id=sm_row.id)
# If the StudentModuleExpand row is new or the StudentModule row was
# more recently updated than the StudentModuleExpand row, fill in/update
# everything and save
if created or (sme.modified < sm_row.modified):
c_updated_rows += 1
sme.grade = sm_row.grade
sme.max_grade = sm_row.max_grade
state = json.loads(sm_row.state)
sme.attempts = state["attempts"]
sme.label = problem_name
sme.username = sm_row.student.username
sme.name = sm_row.student.profile.name
sme.save()
c_all_rows = len(sm_rows)
print "--------------------------------------------------------------------------------"
print "Done! Updated/Created {0} queryable rows out of {1} from courseware_studentmodule".format(
c_updated_rows, c_all_rows)
print "--------------------------------------------------------------------------------"
# Save since everything finished successfully, log latest run.
q_log = Log(script_id=script_id, course_id=course_id, created=tstart)
q_log.save()
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'StudentModuleExpand'
db.create_table('queryable_studentmoduleexpand', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('student_module_id', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)),
('attempts', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)),
('module_type', self.gf('django.db.models.fields.CharField')(default='problem', max_length=32, db_index=True)),
('module_state_key', self.gf('django.db.models.fields.CharField')(max_length=255, db_column='module_id', db_index=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('label', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True)),
('student_id', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)),
('username', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=30, null=True, blank=True)),
('name', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=255, null=True, blank=True)),
('grade', self.gf('django.db.models.fields.FloatField')(db_index=True, null=True, blank=True)),
('max_grade', self.gf('django.db.models.fields.FloatField')(null=True, blank=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
))
db.send_create_signal('queryable_student_module', ['StudentModuleExpand'])
# Adding unique constraint on 'StudentModuleExpand', fields ['student_id', 'module_state_key', 'course_id']
db.create_unique('queryable_studentmoduleexpand', ['student_id', 'module_id', 'course_id'])
# Adding model 'CourseGrade'
db.create_table('queryable_coursegrade', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('percent', self.gf('django.db.models.fields.FloatField')(null=True, db_index=True)),
('grade', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, db_index=True)),
('user_id', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)),
('username', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=30, null=True, blank=True)),
('name', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=255, null=True, blank=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
))
db.send_create_signal('queryable_student_module', ['CourseGrade'])
# Adding unique constraint on 'CourseGrade', fields ['user_id', 'course_id']
db.create_unique('queryable_coursegrade', ['user_id', 'course_id'])
# Adding model 'AssignmentTypeGrade'
db.create_table('queryable_assignmenttypegrade', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('category', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('percent', self.gf('django.db.models.fields.FloatField')(null=True, db_index=True)),
('user_id', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)),
('username', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=30, null=True, blank=True)),
('name', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=255, null=True, blank=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
))
db.send_create_signal('queryable_student_module', ['AssignmentTypeGrade'])
# Adding unique constraint on 'AssignmentTypeGrade', fields ['user_id', 'course_id', 'category']
db.create_unique('queryable_assignmenttypegrade', ['user_id', 'course_id', 'category'])
# Adding model 'AssignmentGrade'
db.create_table('queryable_assignmentgrade', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('category', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('percent', self.gf('django.db.models.fields.FloatField')(null=True, db_index=True)),
('label', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
('detail', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
('user_id', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)),
('username', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=30, null=True, blank=True)),
('name', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=255, null=True, blank=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
))
db.send_create_signal('queryable_student_module', ['AssignmentGrade'])
# Adding unique constraint on 'AssignmentGrade', fields ['user_id', 'course_id', 'label']
db.create_unique('queryable_assignmentgrade', ['user_id', 'course_id', 'label'])
# Adding model 'Log'
db.create_table('queryable_log', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('script_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
))
db.send_create_signal('queryable_student_module', ['Log'])
def backwards(self, orm):
# Removing unique constraint on 'AssignmentGrade', fields ['user_id', 'course_id', 'label']
db.delete_unique('queryable_assignmentgrade', ['user_id', 'course_id', 'label'])
# Removing unique constraint on 'AssignmentTypeGrade', fields ['user_id', 'course_id', 'category']
db.delete_unique('queryable_assignmenttypegrade', ['user_id', 'course_id', 'category'])
# Removing unique constraint on 'CourseGrade', fields ['user_id', 'course_id']
db.delete_unique('queryable_coursegrade', ['user_id', 'course_id'])
# Removing unique constraint on 'StudentModuleExpand', fields ['student_id', 'module_state_key', 'course_id']
db.delete_unique('queryable_studentmoduleexpand', ['student_id', 'module_id', 'course_id'])
# Deleting model 'StudentModuleExpand'
db.delete_table('queryable_studentmoduleexpand')
# Deleting model 'CourseGrade'
db.delete_table('queryable_coursegrade')
# Deleting model 'AssignmentTypeGrade'
db.delete_table('queryable_assignmenttypegrade')
# Deleting model 'AssignmentGrade'
db.delete_table('queryable_assignmentgrade')
# Deleting model 'Log'
db.delete_table('queryable_log')
models = {
'queryable_student_module.assignmentgrade': {
'Meta': {'unique_together': "(('user_id', 'course_id', 'label'),)", 'object_name': 'AssignmentGrade', 'db_table': "'queryable_assignmentgrade'"},
'category': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'detail': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'label': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
'percent': ('django.db.models.fields.FloatField', [], {'null': 'True', 'db_index': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'user_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '30', 'null': 'True', 'blank': 'True'})
},
'queryable_student_module.assignmenttypegrade': {
'Meta': {'unique_together': "(('user_id', 'course_id', 'category'),)", 'object_name': 'AssignmentTypeGrade', 'db_table': "'queryable_assignmenttypegrade'"},
'category': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
'percent': ('django.db.models.fields.FloatField', [], {'null': 'True', 'db_index': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'user_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '30', 'null': 'True', 'blank': 'True'})
},
'queryable_student_module.coursegrade': {
'Meta': {'unique_together': "(('user_id', 'course_id'),)", 'object_name': 'CourseGrade', 'db_table': "'queryable_coursegrade'"},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'grade': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
'percent': ('django.db.models.fields.FloatField', [], {'null': 'True', 'db_index': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'user_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '30', 'null': 'True', 'blank': 'True'})
},
'queryable_student_module.log': {
'Meta': {'ordering': "['-created']", 'object_name': 'Log', 'db_table': "'queryable_log'"},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'script_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
},
'queryable_student_module.studentmoduleexpand': {
'Meta': {'unique_together': "(('student_id', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModuleExpand', 'db_table': "'queryable_studentmoduleexpand'"},
'attempts': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'label': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
'student_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'student_module_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '30', 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['queryable_student_module']
\ No newline at end of file
"""
model file for queryable app
"""
from django.contrib.auth.models import User
from django.db import models
from courseware.models import StudentModule
class StudentModuleExpand(models.Model):
"""
Expanded version of courseware's model StudentModule. This is only for
instances of module type 'problem'. Adds attribute 'attempts' that is pulled
out of the json in the state attribute.
"""
EXPAND_TYPES = {'problem'}
student_module_id = models.IntegerField(blank=True, null=True, db_index=True)
# The value mapped to 'attempts' in the json in state
attempts = models.IntegerField(null=True, blank=True, db_index=True)
# Values from StudentModule
module_type = models.CharField(max_length=32, default='problem', db_index=True)
module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id')
course_id = models.CharField(max_length=255, db_index=True)
label = models.CharField(max_length=50, null=True, blank=True)
student_id = models.IntegerField(blank=True, null=True, db_index=True)
username = models.CharField(max_length=30, blank=True, null=True, db_index=True)
name = models.CharField(max_length=255, blank=True, null=True, db_index=True)
class Meta:
"""
Meta definitions
"""
db_table = "queryable_studentmoduleexpand"
unique_together = (('student_id', 'module_state_key', 'course_id'),)
grade = models.FloatField(null=True, blank=True, db_index=True)
max_grade = models.FloatField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True, db_index=True)
modified = models.DateTimeField(auto_now=True, db_index=True)
class CourseGrade(models.Model):
"""
Holds student's overall course grade as a percentage and letter grade (if letter grade present).
"""
course_id = models.CharField(max_length=255, db_index=True)
percent = models.FloatField(db_index=True, null=True)
grade = models.CharField(max_length=32, db_index=True, null=True)
user_id = models.IntegerField(blank=True, null=True, db_index=True)
username = models.CharField(max_length=30, blank=True, null=True, db_index=True)
name = models.CharField(max_length=255, blank=True, null=True, db_index=True)
class Meta:
"""
Meta definitions
"""
db_table = "queryable_coursegrade"
unique_together = (('user_id', 'course_id'), )
created = models.DateTimeField(auto_now_add=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
class AssignmentTypeGrade(models.Model):
"""
Holds student's average grade for each assignment type per course.
"""
course_id = models.CharField(max_length=255, db_index=True)
category = models.CharField(max_length=255, db_index=True)
percent = models.FloatField(db_index=True, null=True)
user_id = models.IntegerField(blank=True, null=True, db_index=True)
username = models.CharField(max_length=30, blank=True, null=True, db_index=True)
name = models.CharField(max_length=255, blank=True, null=True, db_index=True)
class Meta:
"""
Meta definitions
"""
db_table = "queryable_assignmenttypegrade"
unique_together = (('user_id', 'course_id', 'category'), )
created = models.DateTimeField(auto_now_add=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
class AssignmentGrade(models.Model):
"""
Holds student's assignment grades per course.
"""
course_id = models.CharField(max_length=255, db_index=True)
category = models.CharField(max_length=255, db_index=True)
percent = models.FloatField(db_index=True, null=True)
label = models.CharField(max_length=32, db_index=True)
detail = models.CharField(max_length=255, blank=True, null=True)
user_id = models.IntegerField(blank=True, null=True, db_index=True)
username = models.CharField(max_length=30, blank=True, null=True, db_index=True)
name = models.CharField(max_length=255, blank=True, null=True, db_index=True)
class Meta:
"""
Meta definitions
"""
db_table = "queryable_assignmentgrade"
unique_together = (('user_id', 'course_id', 'label'), )
created = models.DateTimeField(auto_now_add=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
class Log(models.Model):
"""
Log of when a script in this django app was last run. Use to filter out students or rows that don't need to be
processed in the populate scripts and show instructors how fresh the data is.
"""
script_id = models.CharField(max_length=255, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
created = models.DateTimeField(null=True, db_index=True)
class Meta:
"""
Meta definitions
"""
db_table = "queryable_log"
ordering = ["-created"]
get_latest_by = "created"
import json
from datetime import datetime
from pytz import UTC
from mock import Mock, patch
from django.test import TestCase
from django.test.utils import override_settings
from django.core.management import call_command
from courseware import grades
from courseware.tests.factories import StudentModuleFactory
from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from student.tests.factories import UserFactory as StudentUserFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from queryable_student_module.models import Log, CourseGrade, AssignmentTypeGrade, AssignmentGrade
from queryable_student_module.management.commands import populate_studentgrades
class TestPopulateStudentGradesUpdateCourseGrade(TestCase):
"""
Tests the helper fuction update_course_grade in the populate_studentgrades custom command
"""
def setUp(self):
self.course_grade = CourseGrade(percent=0.9, grade='A')
self.gradeset = {'percent': 0.9, 'grade': 'A'}
def test_no_update(self):
"""
Values are the same, so no update
"""
self.assertFalse(populate_studentgrades.update_course_grade(self.course_grade, self.gradeset))
def test_percents_not_equal(self):
"""
Update because the percents don't equal
"""
self.course_grade.percent = 1.0
self.assertTrue(populate_studentgrades.update_course_grade(self.course_grade, self.gradeset))
def test_different_grade(self):
"""
Update because the grade is different
"""
self.course_grade.grade = 'Foo'
self.assertTrue(populate_studentgrades.update_course_grade(self.course_grade, self.gradeset))
def test_grade_as_null(self):
"""
Percent is the same and grade are both null, so no update
"""
self.course_grade.grade = None
self.gradeset['grade'] = None
self.assertFalse(populate_studentgrades.update_course_grade(self.course_grade, self.gradeset))
class TestPopulateStudentGradesGetAssignmentIndex(TestCase):
"""
Tests the helper fuction get_assignment_index in the populate_studentgrades custom command
"""
def test_simple(self):
"""
Simple test if returns correct index.
"""
self.assertEquals(populate_studentgrades.get_assignment_index("HW 3"), 2)
self.assertEquals(populate_studentgrades.get_assignment_index("HW 02"), 1)
self.assertEquals(populate_studentgrades.get_assignment_index("HW 11"), 10)
self.assertEquals(populate_studentgrades.get_assignment_index("HW 001"), 0)
def test_no_index(self):
"""
Test if returns -1 for badly formed input
"""
self.assertEquals(populate_studentgrades.get_assignment_index("HW Avg"), -1)
self.assertEquals(populate_studentgrades.get_assignment_index("HW"), -1)
self.assertEquals(populate_studentgrades.get_assignment_index("HW "), -1)
class TestPopulateStudentGradesGetStudentProblems(TestCase):
"""
Tests the helper fuction get_student_problems in the populate_studentgrades custom command
"""
def setUp(self):
self.student_module = StudentModuleFactory(
module_type='problem',
module_state_key='one',
grade=1,
max_grade=1,
)
def test_single_problem(self):
"""
Test returns a single problem
"""
problem_set = populate_studentgrades.get_student_problems(
self.student_module.course_id,
self.student_module.student,
)
self.assertEquals(len(problem_set), 1)
self.assertEquals(problem_set[0], self.student_module.module_state_key)
def test_problem_with_no_submission(self):
"""
Test to make sure only returns the problems with a submission.
"""
student_module_no_submission = StudentModuleFactory(
course_id=self.student_module.course_id,
student=self.student_module.student,
module_type='problem',
module_state_key='no_submission',
grade=None,
max_grade=None,
)
problem_set = populate_studentgrades.get_student_problems(
self.student_module.course_id,
self.student_module.student,
)
self.assertEquals(len(problem_set), 1)
self.assertEquals(problem_set[0], self.student_module.module_state_key)
class TestPopulateStudentGradesAssignmentExistsAndHasProblems(TestCase):
"""
Tests the helper fuction assignment_exists_and_has_prob in the populate_studentgrades custom command
"""
def setUp(self):
self.category = 'HW'
self.assignment_problems_map = {
self.category: [
['cat_1_problem_id_1'],
]
}
def test_simple(self):
"""
Test where assignment does exist and has problems
"""
self.assertTrue(populate_studentgrades.assignment_exists_and_has_prob(
self.assignment_problems_map,
self.category,
len(self.assignment_problems_map[self.category]) - 1, )
)
def test_assignment_exist_no_problems(self):
"""
Test where assignment exists but has no problems
"""
self.assignment_problems_map['Final'] = [[]]
self.assertFalse(populate_studentgrades.assignment_exists_and_has_prob(
self.assignment_problems_map, 'Final', 0)
)
def test_negative_index(self):
"""
Test handles negative indexes well by returning False
"""
self.assertFalse(populate_studentgrades.assignment_exists_and_has_prob({}, "", -1))
self.assertFalse(populate_studentgrades.assignment_exists_and_has_prob({}, "", -5))
def test_non_existing_category(self):
"""
Test handled a category that doesn't actually exist by returning False
"""
self.assertFalse(populate_studentgrades.assignment_exists_and_has_prob({}, "Foo", 0))
self.assertFalse(populate_studentgrades.assignment_exists_and_has_prob(self.assignment_problems_map, "Foo", 0))
def test_index_too_high(self):
"""
Test that if the index is higher than the actual number of assignments
"""
self.assertFalse(populate_studentgrades.assignment_exists_and_has_prob(
self.assignment_problems_map, self.category, len(self.assignment_problems_map[self.category])))
class TestPopulateStudentGradesStudentDidProblems(TestCase):
"""
Tests the helper fuction student_did_problems in the populate_studentgrades custom command
"""
def setUp(self):
self.student_problems = ['cat_1_problem_1']
def test_student_did_do_problems(self):
"""
Test where student did do some of the problems
"""
self.assertTrue(populate_studentgrades.student_did_problems(self.student_problems, self.student_problems))
problem_set = list(self.student_problems)
problem_set.append('cat_2_problem_1')
self.assertTrue(populate_studentgrades.student_did_problems(self.student_problems, problem_set))
def test_student_did_not_do_problems(self):
"""
Test where student didn't do any problems in the list
"""
self.assertFalse(populate_studentgrades.student_did_problems(self.student_problems, []))
self.assertFalse(populate_studentgrades.student_did_problems([], self.student_problems))
problem_set = ['cat_1_problem_2']
self.assertFalse(populate_studentgrades.student_did_problems(self.student_problems, problem_set))
class TestPopulateStudentGradesStoreCourseGradeIfNeed(TestCase):
"""
Tests the helper fuction store_course_grade_if_need in the populate_studentgrades custom command
"""
def setUp(self):
self.student = StudentUserFactory()
self.course_id = 'test/test/test'
self.gradeset = {
'percent': 1.0,
'grade': 'A',
}
self.course_grade = CourseGrade(
user_id=self.student.id,
course_id=self.course_id,
percent=self.gradeset['percent'],
grade=self.gradeset['grade'],
)
self.course_grade.save()
def test_new_course_grade_store(self):
"""
Test stores because it's a new CourseGrade
"""
self.assertEqual(len(CourseGrade.objects.filter(course_id__exact=self.course_id)), 1)
student = StudentUserFactory()
return_value = populate_studentgrades.store_course_grade_if_need(
student, self.course_id, self.gradeset
)
self.assertTrue(return_value)
self.assertEqual(len(CourseGrade.objects.filter(course_id__exact=self.course_id)), 2)
@patch('queryable_student_module.management.commands.populate_studentgrades.update_course_grade')
def test_update_store(self, mock_update_course_grade):
"""
Test stores because update_course_grade returns True
"""
mock_update_course_grade.return_value = True
updated_time = self.course_grade.updated
return_value = populate_studentgrades.store_course_grade_if_need(
self.student, self.course_id, self.gradeset
)
self.assertTrue(return_value)
course_grades = CourseGrade.objects.filter(
course_id__exact=self.course_id,
user_id=self.student.id,
)
self.assertEqual(len(course_grades), 1)
self.assertNotEqual(updated_time, course_grades[0].updated)
@patch('queryable_student_module.management.commands.populate_studentgrades.update_course_grade')
def test_no_update_no_store(self, mock_update_course_grade):
"""
Test doesn't touch the row because it is not newly created and update_course_grade returns False
"""
mock_update_course_grade.return_value = False
updated_time = self.course_grade.updated
return_value = populate_studentgrades.store_course_grade_if_need(
self.student, self.course_id, self.gradeset
)
self.assertFalse(return_value)
course_grades = CourseGrade.objects.filter(
course_id__exact=self.course_id,
user_id=self.student.id,
)
self.assertEqual(len(course_grades), 1)
self.assertEqual(updated_time, course_grades[0].updated)
class TestPopulateStudentGradesStoreAssignmentTypeGradeIfNeed(TestCase):
"""
Tests the helper fuction store_assignment_type_grade in the populate_studentgrades custom command
"""
def setUp(self):
self.student = StudentUserFactory()
self.course_id = 'test/test/test'
self.category = 'Homework'
self.percent = 1.0
self.assignment_type_grade = AssignmentTypeGrade(
user_id=self.student.id,
username=self.student.username,
name=self.student.profile.name,
course_id=self.course_id,
category=self.category,
percent=self.percent,
)
self.assignment_type_grade.save()
def test_new_assignment_type_grade_store(self):
"""
Test the function both stores the new assignment type grade and returns True meaning that it had
"""
self.assertEqual(len(AssignmentTypeGrade.objects.filter(course_id__exact=self.course_id)), 1)
return_value = populate_studentgrades.store_assignment_type_grade(
self.student, self.course_id, 'Foo 01', 1.0
)
self.assertTrue(return_value)
self.assertEqual(len(AssignmentTypeGrade.objects.filter(course_id__exact=self.course_id)), 2)
def test_difference_percent_store(self):
"""
Test updates the percent value when it is different
"""
new_percent = self.percent - 0.1
return_value = populate_studentgrades.store_assignment_type_grade(
self.student, self.course_id, self.category, new_percent
)
self.assertTrue(return_value)
assignment_type_grades = AssignmentTypeGrade.objects.filter(
course_id__exact=self.course_id,
user_id=self.student.id,
category=self.category,
)
self.assertEqual(len(assignment_type_grades), 1)
self.assertEqual(assignment_type_grades[0].percent, new_percent)
def test_same_percent_no_store(self):
"""
Test does not touch row if the row exists and the precent is not different
"""
updated_time = self.assignment_type_grade.updated
return_value = populate_studentgrades.store_assignment_type_grade(
self.student, self.course_id, self.category, self.percent
)
self.assertFalse(return_value)
assignment_type_grades = AssignmentTypeGrade.objects.filter(
course_id__exact=self.course_id,
user_id=self.student.id,
category=self.category,
)
self.assertEqual(len(assignment_type_grades), 1)
self.assertEqual(assignment_type_grades[0].percent, self.percent)
self.assertEqual(assignment_type_grades[0].updated, updated_time)
class TestPopulateStudentGradesStoreAssignmentGradeIfNeed(TestCase):
"""
Tests the helper fuction store_assignment_grade_if_need in the populate_studentgrades custom command
"""
def setUp(self):
self.student = StudentUserFactory()
self.course_id = 'test/test/test'
self.label = 'HW 01'
self.percent = 1.0
self.assignment_grade = AssignmentGrade(
user_id=self.student.id,
username=self.student.username,
name=self.student.profile.name,
course_id=self.course_id,
label=self.label,
percent=self.percent,
)
self.assignment_grade.save()
def test_new_assignment_grade_store(self):
"""
Test the function both stores the new assignment grade and returns True meaning that it had
"""
self.assertEqual(len(AssignmentGrade.objects.filter(course_id__exact=self.course_id)), 1)
return_value = populate_studentgrades.store_assignment_grade_if_need(
self.student, self.course_id, 'Foo 01', 1.0
)
self.assertTrue(return_value)
self.assertEqual(len(AssignmentGrade.objects.filter(course_id__exact=self.course_id)), 2)
def test_difference_percent_store(self):
"""
Test updates the percent value when it is different
"""
new_percent = self.percent - 0.1
return_value = populate_studentgrades.store_assignment_grade_if_need(
self.student, self.course_id, self.label, new_percent
)
self.assertTrue(return_value)
assignment_grades = AssignmentGrade.objects.filter(
course_id__exact=self.course_id,
user_id=self.student.id,
label=self.label,
)
self.assertEqual(len(assignment_grades), 1)
self.assertEqual(assignment_grades[0].percent, new_percent)
def test_same_percent_no_store(self):
"""
Test does not touch row if the row exists and the precent is not different
"""
updated_time = self.assignment_grade.updated
return_value = populate_studentgrades.store_assignment_grade_if_need(
self.student, self.course_id, self.label, self.percent
)
self.assertFalse(return_value)
assignment_grades = AssignmentGrade.objects.filter(
course_id__exact=self.course_id,
user_id=self.student.id,
label=self.label,
)
self.assertEqual(len(assignment_grades), 1)
self.assertEqual(assignment_grades[0].percent, self.percent)
self.assertEqual(assignment_grades[0].updated, updated_time)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestPopulateStudentGradesCommand(ModuleStoreTestCase):
def create_studentmodule(self):
"""
Creates a StudentModule. This can't be in setUp because some functions can't have one in the database.
"""
sm = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
state=json.dumps({'attempts': 1}),
)
def create_log_entry(self):
"""
Adds a queryable log entry to the database
"""
log = Log(script_id=self.script_id, course_id=self.course.id, created=datetime.now(UTC))
log.save()
def setUp(self):
self.command = 'populate_studentgrades'
self.script_id = 'studentgrades'
self.course = CourseFactory.create()
self.category = 'Homework'
self.gradeset = {
'percent': 1.0,
'grade': 'A',
'section_breakdown': [
{'category': self.category, 'label': 'HW Avg', 'percent': 1.0, 'prominent': True},
{'category': self.category, 'label': 'HW 01', 'percent': 1.0},
],
}
# Make sure these are correct with the above gradeset
self.assignment_type_index = 0
self.assignment_index = 1
def test_missing_input(self):
"""
Fails safely when not given enough input
"""
try:
call_command(self.command)
self.assertTrue(True)
except:
self.assertTrue(False)
def test_just_logs_if_empty_course(self):
"""
If the course has nothing in it, just logs the run in the log table.
"""
call_command(self.command, self.course.id)
self.assertEqual(len(Log.objects.filter(script_id__exact=self.script_id, course_id__exact=self.course.id)), 1)
self.assertEqual(len(CourseGrade.objects.filter(course_id__exact=self.course.id)), 0)
self.assertEqual(len(AssignmentTypeGrade.objects.filter(course_id__exact=self.course.id)), 0)
self.assertEqual(len(AssignmentGrade.objects.filter(course_id__exact=self.course.id)), 0)
@patch('courseware.grades.grade')
def test_force_update(self, mock_grade):
"""
Even if there is a log entry for incremental update, force a full update
This may be done because something happened in the last update.
"""
mock_grade.return_value = self.gradeset
# Create a StudentModule that is before the log entry
sm = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
state=json.dumps({'attempts': 1}),
)
self.create_log_entry()
call_command(self.command, self.course.id, force=True)
self.assertEqual(len(Log.objects.filter(script_id__exact=self.script_id, course_id__exact=self.course.id)), 2)
self.assertEqual(len(CourseGrade.objects.filter(user_id=sm.student.id, course_id__exact=self.course.id)), 1)
self.assertEqual(len(AssignmentTypeGrade.objects.filter(
user_id=sm.student.id, course_id__exact=self.course.id, category=self.category)), 1)
self.assertEqual(len(AssignmentGrade.objects.filter(
user_id=sm.student.id,
course_id__exact=self.course.id,
label=self.gradeset['section_breakdown'][self.assignment_index]['label'], )), 1)
@patch('courseware.grades.grade')
def test_incremental_update_if_log_exists(self, mock_grade):
"""
Make sure it uses the log entry if it exists and we aren't forcing a full update
"""
mock_grade.return_value = self.gradeset
# Create a StudentModule that is before the log entry
sm = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
state=json.dumps({'attempts': 1}),
)
sm.student.last_name = "Student1"
sm.student.save()
self.create_log_entry()
# Create a StudentModule that is after the log entry, different name
sm = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
state=json.dumps({'attempts': 1}),
)
sm.student.last_name = "Student2"
sm.student.save()
call_command(self.command, self.course.id)
self.assertEqual(mock_grade.call_count, 1)
@patch('queryable_student_module.management.commands.populate_studentgrades.store_course_grade_if_need')
@patch('courseware.grades.grade')
def test_store_course_grade(self, mock_grade, mock_method):
"""
Calls store_course_grade_if_need for all students
"""
mock_grade.return_value = self.gradeset
self.create_studentmodule()
call_command(self.command, self.course.id)
self.assertEqual(mock_method.call_count, 1)
@patch('queryable_student_module.management.commands.populate_studentgrades.store_assignment_type_grade')
@patch('courseware.grades.grade')
def test_store_assignment_type_grade(self, mock_grade, mock_method):
"""
Calls store_assignment_type_grade when such a section exists
"""
mock_grade.return_value = self.gradeset
self.create_studentmodule()
call_command(self.command, self.course.id)
self.assertEqual(mock_method.call_count, 1)
@patch('queryable_student_module.management.commands.populate_studentgrades.store_assignment_grade_if_need')
@patch('courseware.grades.grade')
def test_store_assignment_grade_percent_not_zero(self, mock_grade, mock_method):
"""
Calls store_assignment_grade_if_need when the percent for that assignment is not zero
"""
mock_grade.return_value = self.gradeset
self.create_studentmodule()
call_command(self.command, self.course.id)
self.assertEqual(mock_method.call_count, 1)
@patch('queryable_student_module.management.commands.populate_studentgrades.get_assignment_index')
@patch('queryable_student_module.management.commands.populate_studentgrades.store_assignment_grade_if_need')
@patch('courseware.grades.grade')
def test_assignment_grade_percent_zero_bad_index(self, mock_grade, mock_method, mock_assign_index):
"""
Does not call store_assignment_grade_if_need when the percent is zero because get_assignment_index returns a
negative number.
"""
self.gradeset['section_breakdown'][self.assignment_index]['percent'] = 0.0
mock_grade.return_value = self.gradeset
mock_assign_index.return_value = -1
self.create_studentmodule()
call_command(self.command, self.course.id)
self.assertEqual(mock_grade.call_count, 1)
self.assertEqual(mock_method.call_count, 0)
@patch('queryable_student_module.management.commands.populate_studentgrades.get_student_problems')
@patch('queryable_student_module.management.commands.populate_studentgrades.assignment_exists_and_has_prob')
@patch('queryable_student_module.util.get_assignment_to_problem_map')
@patch('queryable_student_module.management.commands.populate_studentgrades.store_assignment_grade_if_need')
@patch('courseware.grades.grade')
def test_assignment_grade_percent_zero_no_student_problems(self, mock_grade, mock_method, mock_assign_problem_map,
mock_assign_exists, mock_student_problems):
"""
Does not call store_assignment_grade_if_need when the percent is zero because the student did not submit
answers to any problems in that assignment.
"""
self.gradeset['section_breakdown'][self.assignment_index]['percent'] = 0.0
mock_grade.return_value = self.gradeset
mock_assign_problem_map.return_value = {
self.gradeset['section_breakdown'][self.assignment_index]['category']: [[]]
}
mock_assign_exists.return_value = True
mock_student_problems.return_value = []
self.create_studentmodule()
call_command(self.command, self.course.id)
self.assertEqual(mock_method.call_count, 0)
@patch('queryable_student_module.management.commands.populate_studentgrades.get_student_problems')
@patch('queryable_student_module.management.commands.populate_studentgrades.assignment_exists_and_has_prob')
@patch('queryable_student_module.util.get_assignment_to_problem_map')
@patch('queryable_student_module.management.commands.populate_studentgrades.store_assignment_grade_if_need')
@patch('courseware.grades.grade')
def test_assignment_grade_percent_zero_has_student_problems(self, mock_grade, mock_method, mock_assign_problem_map,
mock_assign_exists, mock_student_problems):
"""
Calls store_assignment_grade_if_need when the percent is zero because the student did submit answers to
problems in that assignment.
"""
self.gradeset['section_breakdown'][self.assignment_index]['percent'] = 0.0
mock_grade.return_value = self.gradeset
mock_assign_problem_map.return_value = {
self.gradeset['section_breakdown'][self.assignment_index]['category']: [['problem_1']]
}
mock_assign_exists.return_value = True
mock_student_problems.return_value = ['problem_1']
self.create_studentmodule()
call_command(self.command, self.course.id)
self.assertEqual(mock_method.call_count, 1)
import json
from datetime import datetime
from pytz import UTC
from StringIO import StringIO
from django.test import TestCase
from django.test.utils import override_settings
from django.core import management
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from courseware.tests.factories import StudentModuleFactory
from queryable_student_module.models import Log, StudentModuleExpand
from capa.tests.response_xml_factory import StringResponseXMLFactory
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestPopulateStudentModuleExpand(TestCase):
def setUp(self):
self.command = 'populate_studentmoduleexpand'
self.script_id = "studentmoduleexpand"
#self.course_id = 'test/test/test'
self.course = CourseFactory.create()
section = ItemFactory.create(
parent_location=self.course.location,
category="chapter",
display_name="test factory section",
)
sub_section = ItemFactory.create(
parent_location=section.location,
category="sequential",
# metadata={'graded': True, 'format': 'Homework'}
)
unit = ItemFactory.create(
parent_location=sub_section.location,
category="vertical",
metadata={'graded': True, 'format': 'Homework'}
)
category = "problem"
self.item = ItemFactory.create(
parent_location=unit.location,
category=category,
data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'}
)
self.item2 = ItemFactory.create(
parent_location=unit.location,
category=category,
data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'}
)
def test_missing_input(self):
"""
Fails safely when not given enough input
"""
try:
management.call_command(self.command)
self.assertTrue(True)
except:
self.assertTrue(False)
def test_just_logs_if_empty_course(self):
"""
If the course has nothing in it, just logs the run in the log table
"""
management.call_command(self.command, self.course.id)
self.assertEqual(len(Log.objects.filter(script_id__exact=self.script_id, course_id__exact=self.course.id)), 1)
self.assertEqual(len(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)), 0)
def test_force_update(self):
"""
Even if there is a log entry for incremental update, force a full update
This may be done because something happened in the last update.
"""
# Create a StudentModule that is before the log entry
sm = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
state=json.dumps({'attempts': 1}),
module_state_key=self.item.location
)
# Create the log entry
log = Log(script_id=self.script_id, course_id=self.course.id, created=datetime.now(UTC))
log.save()
# Create a StudentModuleExpand that is after the log entry and has a different attempts value
sme = StudentModuleExpand(
course_id=self.course.id,
module_state_key=sm.module_state_key,
student_module_id=sm.id,
attempts=0,
)
# Call command with the -f flag
management.call_command(self.command, self.course.id, force=True)
# Check to see if new rows have been added
self.assertEqual(len(Log.objects.filter(script_id__exact=self.script_id, course_id__exact=self.course.id)), 2)
self.assertEqual(len(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)), 1)
self.assertEqual(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)[0].attempts, 1)
def test_incremental_update_if_log_exists(self):
"""
Make sure it uses the log entry if it exists and we aren't forcing a full update
"""
# Create a StudentModule that is before the log entry
sm = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
state=json.dumps({'attempts': 1}),
module_state_key=self.item.location
)
# Create the log entry
log = Log(script_id=self.script_id, course_id=self.course.id, created=datetime.now(UTC))
log.save()
# Create a StudentModule that is after the log entry
sm = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
state=json.dumps({'attempts': 1}),
module_state_key=self.item.location
)
# Call command
management.call_command(self.command, self.course.id)
# Check to see if new row has been added to log
self.assertEqual(len(Log.objects.filter(script_id__exact=self.script_id, course_id__exact=self.course.id)), 2)
# Even though there are two studentmodules only one row should be created
self.assertEqual(len(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)), 1)
def test_update_only_if_row_modified(self):
"""
Test populate does not update a row if it is not necessary
For example the problem may have a more recent modified date but the attempts value has not changed.
"""
self.assertEqual(len(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)), 0)
# Create a StudentModule
sm1 = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
grade=1,
max_grade=1,
module_state_key=self.item.location
)
# Create a StudentModuleExpand
sme1 = StudentModuleExpand(
course_id=self.course.id,
student_module_id=sm1.id,
module_state_key=sm1.module_state_key,
student_id=sm1.student.id,
attempts=0,
)
sme1.save()
# Touch the StudentModule row so it has a later modified time
sm1.state = json.dumps({'attempts': 1})
sm1.save()
# Create a StudentModule
sm2 = StudentModuleFactory(
course_id=self.course.id,
module_type='problem',
module_state_key=self.item2.location,
grade=1,
max_grade=1,
state=json.dumps({'attempts': 2}),
)
# Create a StudentModuleExpand that has the same attempts value
sme2 = StudentModuleExpand(
course_id=self.course.id,
student_module_id=sm2.id,
module_state_key=sm2.module_state_key,
student_id=sm2.student.id,
attempts=2,
)
sme2.save()
self.assertEqual(len(StudentModuleExpand.objects.filter(course_id__exact=self.course.id)), 2)
# Call command
management.call_command(self.command, self.course.id)
self.assertEqual(len(StudentModuleExpand.objects.filter(
course_id__exact=self.course.id, module_state_key__exact=sme1.module_state_key)), 1)
self.assertEqual(len(StudentModuleExpand.objects.filter(
course_id__exact=self.course.id, module_state_key__exact=sme2.module_state_key)), 1)
self.assertEqual(StudentModuleExpand.objects.filter(
course_id__exact=self.course.id, module_state_key__exact=sme1.module_state_key)[0].attempts, 1)
self.assertEqual(StudentModuleExpand.objects.filter(
course_id__exact=self.course.id, module_state_key__exact=sme2.module_state_key)[0].attempts, 2)
from django.test import TestCase
from django.test.utils import override_settings
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from queryable_student_module import util
class TestUtilApproxEqual(TestCase):
"""
Check the approx_equal function
"""
def test_default_tolerance(self):
"""
Check that function with default tolerance
"""
self.assertTrue(util.approx_equal(1.00001, 1.0))
self.assertTrue(util.approx_equal(1.0, 1.00001))
self.assertFalse(util.approx_equal(1.0, 2.0))
self.assertFalse(util.approx_equal(1.0, 1.0002))
def test_smaller_default_tolerance(self):
"""
Set tolerance smaller than default and check if still correct
"""
self.assertTrue(util.approx_equal(1.0, 1.0, 1))
self.assertTrue(util.approx_equal(1.0, 1.000001, 0.000001))
def test_bigger_default_tolerance(self):
"""
Set tolerance bigger than default and check if still correct
"""
self.assertFalse(util.approx_equal(1.0, 2.0, 0.75))
self.assertFalse(util.approx_equal(2.0, 1.0, 0.75))
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestUtilGetAssignmentToProblemMap(TestCase):
"""
Tests the get_assignemnt_to_problem_map
"""
def setUp(self):
self.course = CourseFactory.create()
def test_empty_course(self):
"""
Test for course with nothing in it
"""
problems_map = util.get_assignment_to_problem_map(self.course.id)
self.assertEqual(problems_map, {})
def test_single_assignment(self):
"""
Test returns the problems for a course with a single assignment
"""
section = ItemFactory.create(
parent_location=self.course.location.url(),
category="chapter")
subsection = ItemFactory.create(
parent_location=section.location.url(),
category="sequential",
)
subsection_metadata = own_metadata(subsection)
subsection_metadata['graded'] = True
subsection_metadata['format'] = "Homework"
modulestore().update_metadata(subsection.location, subsection_metadata)
unit = ItemFactory.create(
parent_location=subsection.location.url(),
category="vertical",
)
problem1 = ItemFactory.create(
parent_location=unit.location.url(),
category="problem",
)
problem2 = ItemFactory.create(
parent_location=unit.location.url(),
category="problem",
)
problems_map = util.get_assignment_to_problem_map(self.course.id)
answer = {'Homework': [[problem1.location.url(), problem2.location.url()], ],
}
self.assertEqual(problems_map, answer)
def test_two_assignments_same_type(self):
"""
Test if has two assignments
"""
section = ItemFactory.create(
parent_location=self.course.location.url(),
category="chapter")
subsection1 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata1 = own_metadata(subsection1)
subsection_metadata1['graded'] = True
subsection_metadata1['format'] = "Homework"
modulestore().update_metadata(subsection1.location, subsection_metadata1)
unit1 = ItemFactory.create(
parent_location=subsection1.location.url(),
category="vertical")
problem1 = ItemFactory.create(
parent_location=unit1.location.url(),
category="problem")
subsection2 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata2 = own_metadata(subsection2)
subsection_metadata2['graded'] = True
subsection_metadata2['format'] = "Homework"
modulestore().update_metadata(subsection2.location, subsection_metadata2)
unit2 = ItemFactory.create(
parent_location=subsection2.location.url(),
category="vertical")
problem2 = ItemFactory.create(
parent_location=unit2.location.url(),
category="problem")
problems_map = util.get_assignment_to_problem_map(self.course.id)
answer = {'Homework': [[problem1.location.url()], [problem2.location.url()], ],
}
self.assertEqual(problems_map, answer)
def test_two_assignments_different_types(self):
"""
Creates two assignments of different types
"""
section = ItemFactory.create(
parent_location=self.course.location.url(),
category="chapter")
subsection1 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata1 = own_metadata(subsection1)
subsection_metadata1['graded'] = True
subsection_metadata1['format'] = "Homework"
modulestore().update_metadata(subsection1.location, subsection_metadata1)
unit1 = ItemFactory.create(
parent_location=subsection1.location.url(),
category="vertical")
problem1 = ItemFactory.create(
parent_location=unit1.location.url(),
category="problem")
subsection2 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata2 = own_metadata(subsection2)
subsection_metadata2['graded'] = True
subsection_metadata2['format'] = "Quiz"
modulestore().update_metadata(subsection2.location, subsection_metadata2)
unit2 = ItemFactory.create(
parent_location=subsection2.location.url(),
category="vertical")
problem2 = ItemFactory.create(
parent_location=unit2.location.url(),
category="problem")
problems_map = util.get_assignment_to_problem_map(self.course.id)
answer = {'Homework': [[problem1.location.url()], ],
'Quiz': [[problem2.location.url()], ],
}
self.assertEqual(problems_map, answer)
def test_return_only_graded_subsections(self):
"""
Make sure only returns problems and assignments that are graded
"""
section = ItemFactory.create(
parent_location=self.course.location.url(),
category="chapter")
subsection1 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata1 = own_metadata(subsection1)
subsection_metadata1['graded'] = True
subsection_metadata1['format'] = "Homework"
modulestore().update_metadata(subsection1.location, subsection_metadata1)
unit1 = ItemFactory.create(
parent_location=subsection1.location.url(),
category="vertical")
problem1 = ItemFactory.create(
parent_location=unit1.location.url(),
category="problem")
subsection2 = ItemFactory.create(
parent_location=section.location.url(),
category="sequential")
subsection_metadata2 = own_metadata(subsection2)
subsection_metadata2['format'] = "Quiz"
modulestore().update_metadata(subsection2.location, subsection_metadata2)
unit2 = ItemFactory.create(
parent_location=subsection2.location.url(),
category="vertical")
problem2 = ItemFactory.create(
parent_location=unit2.location.url(),
category="problem")
problems_map = util.get_assignment_to_problem_map(self.course.id)
answer = {'Homework': [[problem1.location.url()], ],
}
self.assertEqual(problems_map, answer)
"""
Utility functions to help with population
"""
from datetime import datetime
from pytz import UTC
import logging
from optparse import make_option
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from queryable_student_module.models import Log
log = logging.getLogger("mitx.queryable")
def get_assignment_to_problem_map(course_id):
"""
Returns a dictionary with assignment types/categories as keys and the value is an array of arrays. Each inner array
holds problem ids for an assignment. The arrays are ordered in the outer array as they are seen in the course, which
is how they are numbered in a student's progress page.
"""
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4)
assignment_problems_map = {}
for section in course.get_children():
for subsection in section.get_children():
subsection_metadata = own_metadata(subsection)
if ('graded' in subsection_metadata) and subsection_metadata['graded']:
category = subsection_metadata['format']
if category not in assignment_problems_map:
assignment_problems_map[category] = []
problems = []
for unit in subsection.get_children():
for child in unit.get_children():
if child.location.category == 'problem':
problems.append(child.location.url())
assignment_problems_map[category].append(problems)
return assignment_problems_map
def approx_equal(first, second, tolerance=0.0001):
"""
Checks if first and second are at most the specified tolerance away from each other.
"""
return abs(first - second) <= tolerance
def pre_run_command(script_id, options, course_id):
"""
Common pre-run method for both populate_studentgrades and populate_studentmoduleexpand commands.
"""
log.info("--------------------------------------------------------------------------------")
log.info("Populating queryable.{0} table for course {1}".format(script_id, course_id))
log.info("--------------------------------------------------------------------------------")
# Grab when we start, to log later
tstart = datetime.now(UTC)
iterative_populate = True
last_log_run = {}
if options['force']:
log.info("--------------------------------------------------------------------------------")
log.info("Full populate: Forced full populate")
log.info("--------------------------------------------------------------------------------")
iterative_populate = False
if iterative_populate:
# Get when this script was last run for this course
last_log_run = Log.objects.filter(script_id__exact=script_id, course_id__exact=course_id)
length = len(last_log_run)
log.info("--------------------------------------------------------------------------------")
if length > 0:
log.info("Iterative populate: Last log run %s", str(last_log_run[0].created))
else:
log.info("Full populate: Can't find log of last run")
iterative_populate = False
log.info("--------------------------------------------------------------------------------")
return iterative_populate, tstart, last_log_run
def more_options():
"""
Appends common options to options list
"""
option_list = make_option('-f', '--force',
action='store_true',
dest='force',
default=False,
help='Forces a full populate for all students and rows, rather than iterative.')
return option_list
\ No newline at end of file
...@@ -298,9 +298,6 @@ FEATURES['CLASS_DASHBOARD'] = True ...@@ -298,9 +298,6 @@ FEATURES['CLASS_DASHBOARD'] = True
import openid.oidutil import openid.oidutil
openid.oidutil.log = lambda message, level = 0: None openid.oidutil.log = lambda message, level = 0: None
### QUERYABLE APP ###
INSTALLED_APPS += ('queryable_student_module',)
# set up some testing for microsites # set up some testing for microsites
MICROSITE_CONFIGURATION = { MICROSITE_CONFIGURATION = {
"test_microsite": { "test_microsite": {
......
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