Commit f5799fe2 by Xavier Antoviaque

Merge pull request #257 from edx-solutions/mattdrayer/rebase-20140926-cherrypicks

Mattdrayer/rebase 20140926 cherrypicks
parents 31a5bc25 385d74c5
...@@ -358,7 +358,7 @@ def _get_course_data(request, course_key, course_descriptor, depth=0): ...@@ -358,7 +358,7 @@ def _get_course_data(request, course_key, course_descriptor, depth=0):
course_descriptor course_descriptor
) )
base_uri_without_qs = generate_base_uri(request, True) base_uri_without_qs = generate_base_uri(request, True)
data['course_image_url'] = request.build_absolute_uri(course_image_url(course_descriptor)) data['course_image_url'] = course_image_url(course_descriptor)
data['resources'] = [] data['resources'] = []
resource_uri = '{}/content/'.format(base_uri_without_qs) resource_uri = '{}/content/'.format(base_uri_without_qs)
data['resources'].append({'uri': resource_uri}) data['resources'].append({'uri': resource_uri})
...@@ -1526,16 +1526,16 @@ class CoursesMetrics(SecureAPIView): ...@@ -1526,16 +1526,16 @@ class CoursesMetrics(SecureAPIView):
'users_started': users_started_qs.values('user').distinct().count(), 'users_started': users_started_qs.values('user').distinct().count(),
'grade_cutoffs': course_descriptor.grading_policy['GRADE_CUTOFFS'] 'grade_cutoffs': course_descriptor.grading_policy['GRADE_CUTOFFS']
} }
# TODO: (mattdrayer) Uncomment after comment service has been updated
# thread_stats = {} thread_stats = {}
# try: try:
# thread_stats = get_course_thread_stats(slash_course_id) thread_stats = get_course_thread_stats(slash_course_id)
# except CommentClientRequestError, e: except CommentClientRequestError, e:
# data = { data = {
# "err_msg": str(e) "err_msg": str(e)
# } }
# return Response(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# data.update(thread_stats) data.update(thread_stats)
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
......
...@@ -147,62 +147,3 @@ def get_course_child_content(request, user, course_key, child_descriptor): ...@@ -147,62 +147,3 @@ def get_course_child_content(request, user, course_key, child_descriptor):
field_data_cache, field_data_cache,
course_key) course_key)
return child_content return child_content
def calculate_proforma_grade(grade_summary, grading_policy):
"""
Calculates a projected (proforma) final grade based on the current state
of grades using the provided grading policy. Categories equate to grading policy
'types' and have values such as 'Homework', 'Lab', 'MidtermExam', and 'FinalExam'
We invert the concepts here and use the category weights as the possible scores by
assuming that the weights total 100 percent. So, if a Homework category is worth 15
percent of your overall grade, and you have currently scored 70 percent for that
category, the normalized score for the Homework category is 0.105. Note that
we do not take into account dropped assignments/scores, such as lowest-two homeworks.
After all scored categories are processed we apply the average category score to any
unscored categories using the value as a projection of the user's performance in each category.
Example:
- Scored Category: Homework, Weight: 15%, Totaled Score: 70%, Normalized Score: 0.105
- Scored Category: MidtermExam, Weight: 30%, Totaled Score: 80%, Normalized Score: 0.240
- Scored Category: Final Exam, Weight: 40%, Totaled Score: 95%, Normalized Score: 0.380
- Average Category Score: (70 + 80 + 95) / 3 = 81.7
- Unscored Category: Lab, Weight: 15%, Totaled Score: 81.7%, Normalized Score: 0.123
- Proforma Grade = 0.105 + 0.240 + 0.380 + 0.123 = 0.8475 (84.8%)
"""
grade_breakdown = grade_summary['grade_breakdown']
proforma_grade = 0.00
totaled_scores = grade_summary['totaled_scores']
category_averages = []
categories_to_estimate = []
for grade_category in grade_breakdown:
category = grade_category['category']
item_scores = totaled_scores.get(category)
if item_scores is not None and len(item_scores):
total_item_score = 0.00
items_considered = 0
for item_score in item_scores:
if item_score.earned or (item_score.due and item_score.due < timezone.now()):
normalized_item_score = item_score.earned / item_score.possible
total_item_score += normalized_item_score
items_considered += 1
if total_item_score:
category_average_score = total_item_score / items_considered
category_averages.append(category_average_score)
category_policy = next((policy for policy in grading_policy['GRADER'] if policy['type'] == category), None)
category_weight = category_policy['weight']
category_grade = category_average_score * category_weight
proforma_grade += category_grade
else:
categories_to_estimate.append(category)
else:
categories_to_estimate.append(category)
assumed_category_average = sum(category_averages) / len(category_averages) if len(category_averages) > 0 else 0
for category in categories_to_estimate:
category_policy = next((policy for policy in grading_policy['GRADER'] if policy['type'] == category), None)
category_weight = category_policy['weight']
category_grade = assumed_category_average * category_weight
proforma_grade += category_grade
return proforma_grade
...@@ -7,12 +7,14 @@ Run these tests @ Devstack: ...@@ -7,12 +7,14 @@ Run these tests @ Devstack:
import json import json
import uuid import uuid
import mock import mock
from urllib import urlencode
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.utils import override_settings from django.test.utils import override_settings
from gradebook.models import StudentGradebook
from student.models import UserProfile from student.models import UserProfile
from student.tests.factories import CourseEnrollmentFactory from student.tests.factories import CourseEnrollmentFactory
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
...@@ -297,3 +299,105 @@ class OrganizationsApiTests(TestCase): ...@@ -297,3 +299,105 @@ class OrganizationsApiTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data[0]['id'], self.test_user.id) self.assertEqual(response.data[0]['id'], self.test_user.id)
self.assertEqual(response.data[0]['course_count'], 2) self.assertEqual(response.data[0]['course_count'], 2)
def test_organizations_metrics_get(self):
users = []
for i in xrange(1, 6):
data = {
'email': 'test{}@example.com'.format(i),
'username': 'test_user{}'.format(i),
'password': 'test_pass',
'first_name': 'John{}'.format(i),
'last_name': 'Doe{}'.format(i),
'city': 'Boston',
}
response = self.do_post(self.base_users_uri, data)
self.assertEqual(response.status_code, 201)
user_id = response.data['id']
user = User.objects.get(pk=user_id)
users.append(user_id)
if i < 2:
StudentGradebook.objects.create(user=user, grade=0.75, proforma_grade=0.85)
elif i < 4:
StudentGradebook.objects.create(user=user, grade=0.82, proforma_grade=0.82)
else:
StudentGradebook.objects.create(user=user, grade=0.90, proforma_grade=0.91)
data = {
'name': self.test_organization_name,
'display_name': self.test_organization_display_name,
'contact_name': self.test_organization_contact_name,
'contact_email': self.test_organization_contact_email,
'contact_phone': self.test_organization_contact_phone,
'logo_url': self.test_organization_logo_url,
'users': users
}
response = self.do_post(self.base_organizations_uri, data)
test_uri = '{}{}/'.format(self.base_organizations_uri, str(response.data['id']))
users_uri = '{}users/'.format(test_uri)
metrics_uri = '{}metrics/'.format(test_uri)
response = self.do_get(metrics_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['users_grade_complete_count'], 4)
def test_organizations_metrics_get_courses_filter(self):
users = []
for i in xrange(1, 10):
data = {
'email': 'test{}@example.com'.format(i),
'username': 'test_user{}'.format(i),
'password': 'test_pass',
'first_name': 'John{}'.format(i),
'last_name': 'Doe{}'.format(i),
'city': 'Boston',
}
response = self.do_post(self.base_users_uri, data)
self.assertEqual(response.status_code, 201)
user_id = response.data['id']
user = User.objects.get(pk=user_id)
users.append(user_id)
course1 = CourseFactory.create(display_name="COURSE1", org="CRS1", run="RUN1")
course2 = CourseFactory.create(display_name="COURSE2", org="CRS2", run="RUN2")
course3 = CourseFactory.create(display_name="COURSE3", org="CRS3", run="RUN3")
if i < 3:
StudentGradebook.objects.create(user=user, grade=0.75, proforma_grade=0.85, course_id=course1.id)
elif i < 5:
StudentGradebook.objects.create(user=user, grade=0.82, proforma_grade=0.82, course_id=course2.id)
elif i < 7:
StudentGradebook.objects.create(user=user, grade=0.72, proforma_grade=0.78, course_id=course3.id)
elif i < 9:
StudentGradebook.objects.create(user=user, grade=0.94, proforma_grade=0.67, course_id=course1.id)
else:
StudentGradebook.objects.create(user=user, grade=0.90, proforma_grade=0.91, course_id=course2.id)
data = {
'name': self.test_organization_name,
'display_name': self.test_organization_display_name,
'contact_name': self.test_organization_contact_name,
'contact_email': self.test_organization_contact_email,
'contact_phone': self.test_organization_contact_phone,
'logo_url': self.test_organization_logo_url,
'users': users
}
response = self.do_post(self.base_organizations_uri, data)
test_uri = '{}{}/'.format(self.base_organizations_uri, str(response.data['id']))
users_uri = '{}users/'.format(test_uri)
metrics_uri = '{}metrics/'.format(test_uri)
response = self.do_get(metrics_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['users_grade_complete_count'], 5)
courses = {'courses': unicode(course1.id)}
filtered_metrics_uri = '{}?{}'.format(metrics_uri, urlencode(courses))
response = self.do_get(filtered_metrics_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['users_grade_complete_count'], 2)
courses = {'courses': '{},{}'.format(course1.id, course2.id)}
filtered_metrics_uri = '{}?{}'.format(metrics_uri, urlencode(courses))
response = self.do_get(filtered_metrics_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['users_grade_complete_count'], 5)
# pylint: disable=C0103 # pylint: disable=C0103
""" ORGANIZATIONS API VIEWS """ """ ORGANIZATIONS API VIEWS """
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import F
from rest_framework import status, viewsets from rest_framework import status, viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from api_manager.courseware_access import get_course_key
from api_manager.models import Organization from api_manager.models import Organization
from api_manager.users.serializers import UserSerializer from api_manager.users.serializers import UserSerializer
from api_manager.utils import str2bool from api_manager.utils import str2bool
from gradebook.models import StudentGradebook
from student.models import CourseEnrollment from student.models import CourseEnrollment
from .serializers import OrganizationSerializer from .serializers import OrganizationSerializer
...@@ -23,6 +27,27 @@ class OrganizationsViewSet(viewsets.ModelViewSet): ...@@ -23,6 +27,27 @@ class OrganizationsViewSet(viewsets.ModelViewSet):
serializer_class = OrganizationSerializer serializer_class = OrganizationSerializer
model = Organization model = Organization
@action(methods=['get',])
def metrics(self, request, pk):
"""
Provide statistical information for the specified Organization
"""
response_data = {}
grade_complete_match_range = getattr(settings, 'GRADEBOOK_GRADE_COMPLETE_PROFORMA_MATCH_RANGE', 0.01)
org_user_grades = StudentGradebook.objects.filter(user__organizations=pk)
courses_filter = request.QUERY_PARAMS.get('courses', None)
if courses_filter:
upper_bound = getattr(settings, 'API_LOOKUP_UPPER_BOUND', 100)
courses_filter = courses_filter.split(",")[:upper_bound]
courses = []
for course_string in courses_filter:
courses.append(get_course_key(course_string))
org_user_grades = org_user_grades.filter(course_id__in=courses)
users_grade_complete_count = org_user_grades\
.filter(proforma_grade__lte=F('grade') + grade_complete_match_range).count()
response_data['users_grade_complete_count'] = users_grade_complete_count
return Response(response_data, status=status.HTTP_200_OK)
@action(methods=['get', 'post']) @action(methods=['get', 'post'])
def users(self, request, pk): def users(self, request, pk):
""" """
......
...@@ -46,7 +46,7 @@ from xmodule.modulestore import InvalidLocationError ...@@ -46,7 +46,7 @@ from xmodule.modulestore import InvalidLocationError
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from api_manager.courses.serializers import CourseModuleCompletionSerializer from api_manager.courses.serializers import CourseModuleCompletionSerializer
from api_manager.courseware_access import get_course, get_course_child, get_course_child_content, get_course_key, course_exists, calculate_proforma_grade from api_manager.courseware_access import get_course, get_course_child, get_course_key, course_exists
from api_manager.permissions import SecureAPIView, SecureListAPIView, IdsInFilterBackend, HasOrgsFilterBackend from api_manager.permissions import SecureAPIView, SecureListAPIView, IdsInFilterBackend, HasOrgsFilterBackend
from api_manager.models import GroupProfile, APIUser as User from api_manager.models import GroupProfile, APIUser as User
from api_manager.organizations.serializers import OrganizationSerializer from api_manager.organizations.serializers import OrganizationSerializer
...@@ -947,7 +947,7 @@ class UsersCoursesGradesDetail(SecureAPIView): ...@@ -947,7 +947,7 @@ class UsersCoursesGradesDetail(SecureAPIView):
) )
if len(queryset): if len(queryset):
current_grade = queryset[0].grade current_grade = queryset[0].grade
proforma_grade = calculate_proforma_grade(grade_summary, grading_policy) proforma_grade = grades.calculate_proforma_grade(grade_summary, grading_policy)
response_data = { response_data = {
'courseware_summary': progress_summary, 'courseware_summary': progress_summary,
......
...@@ -9,6 +9,7 @@ from contextlib import contextmanager ...@@ -9,6 +9,7 @@ from contextlib import contextmanager
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.utils import timezone
from dogapi import dog_stats_api from dogapi import dog_stats_api
...@@ -546,3 +547,62 @@ def iterate_grades_for(course_id, students): ...@@ -546,3 +547,62 @@ def iterate_grades_for(course_id, students):
exc.message exc.message
) )
yield student, {}, exc.message yield student, {}, exc.message
def calculate_proforma_grade(grade_summary, grading_policy):
"""
Calculates a projected (proforma) final grade based on the current state
of grades using the provided grading policy. Categories equate to grading policy
'types' and have values such as 'Homework', 'Lab', 'MidtermExam', and 'FinalExam'
We invert the concepts here and use the category weights as the possible scores by
assuming that the weights total 100 percent. So, if a Homework category is worth 15
percent of your overall grade, and you have currently scored 70 percent for that
category, the normalized score for the Homework category is 0.105. Note that
we do not take into account dropped assignments/scores, such as lowest-two homeworks.
After all scored categories are processed we apply the average category score to any
unscored categories using the value as a projection of the user's performance in each category.
Example:
- Scored Category: Homework, Weight: 15%, Totaled Score: 70%, Normalized Score: 0.105
- Scored Category: MidtermExam, Weight: 30%, Totaled Score: 80%, Normalized Score: 0.240
- Scored Category: Final Exam, Weight: 40%, Totaled Score: 95%, Normalized Score: 0.380
- Average Category Score: (70 + 80 + 95) / 3 = 81.7
- Unscored Category: Lab, Weight: 15%, Totaled Score: 81.7%, Normalized Score: 0.123
- Proforma Grade = 0.105 + 0.240 + 0.380 + 0.123 = 0.8475 (84.8%)
"""
grade_breakdown = grade_summary['grade_breakdown']
proforma_grade = 0.00
totaled_scores = grade_summary['totaled_scores']
category_averages = []
categories_to_estimate = []
for grade_category in grade_breakdown:
category = grade_category['category']
item_scores = totaled_scores.get(category)
if item_scores is not None and len(item_scores):
total_item_score = 0.00
items_considered = 0
for item_score in item_scores:
if item_score.earned or (item_score.due and item_score.due < timezone.now()):
normalized_item_score = item_score.earned / item_score.possible
total_item_score += normalized_item_score
items_considered += 1
if total_item_score:
category_average_score = total_item_score / items_considered
category_averages.append(category_average_score)
category_policy = next((policy for policy in grading_policy['GRADER'] if policy['type'] == category), None)
category_weight = category_policy['weight']
category_grade = category_average_score * category_weight
proforma_grade += category_grade
else:
categories_to_estimate.append(category)
else:
categories_to_estimate.append(category)
assumed_category_average = sum(category_averages) / len(category_averages) if len(category_averages) > 0 else 0
for category in categories_to_estimate:
category_policy = next((policy for policy in grading_policy['GRADER'] if policy['type'] == category), None)
category_weight = category_policy['weight']
category_grade = assumed_category_average * category_weight
proforma_grade += category_grade
return proforma_grade
...@@ -69,13 +69,15 @@ class Command(BaseCommand): ...@@ -69,13 +69,15 @@ class Command(BaseCommand):
request.user = user request.user = user
grade_data = grades.grade(user, request, course) grade_data = grades.grade(user, request, course)
grade = grade_data['percent'] grade = grade_data['percent']
proforma_grade = grades.calculate_proforma_grade(grade_data, course.grading_policy)
try: try:
gradebook_entry = StudentGradebook.objects.get(user=user, course_id=course.id) gradebook_entry = StudentGradebook.objects.get(user=user, course_id=course.id)
if gradebook_entry.grade != grade: if gradebook_entry.grade != grade:
gradebook_entry.grade = grade gradebook_entry.grade = grade
proforma_grade = proforma_grade
gradebook_entry.save() gradebook_entry.save()
except StudentGradebook.DoesNotExist: except StudentGradebook.DoesNotExist:
StudentGradebook.objects.create(user=user, course_id=course.id, grade=grade) StudentGradebook.objects.create(user=user, course_id=course.id, grade=grade, proforma_grade=proforma_grade)
log_msg = 'Gradebook entry created -- Course: {}, User: {} (grade: {})'.format(course.id, user.id, grade) log_msg = 'Gradebook entry created -- Course: {}, User: {} (grade: {})'.format(course.id, user.id, grade)
print log_msg print log_msg
log.info(log_msg) log.info(log_msg)
# -*- 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 field 'StudentGradebookHistory.proforma_grade'
db.add_column('gradebook_studentgradebookhistory', 'proforma_grade',
self.gf('django.db.models.fields.FloatField')(default=0),
keep_default=False)
# Adding field 'StudentGradebook.proforma_grade'
db.add_column('gradebook_studentgradebook', 'proforma_grade',
self.gf('django.db.models.fields.FloatField')(default=0),
keep_default=False)
def backwards(self, orm):
# Deleting field 'StudentGradebookHistory.proforma_grade'
db.delete_column('gradebook_studentgradebookhistory', 'proforma_grade')
# Deleting field 'StudentGradebook.proforma_grade'
db.delete_column('gradebook_studentgradebook', 'proforma_grade')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'gradebook.studentgradebook': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'StudentGradebook'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'grade': ('django.db.models.fields.FloatField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proforma_grade': ('django.db.models.fields.FloatField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'gradebook.studentgradebookhistory': {
'Meta': {'object_name': 'StudentGradebookHistory'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'grade': ('django.db.models.fields.FloatField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'proforma_grade': ('django.db.models.fields.FloatField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
complete_apps = ['gradebook']
\ No newline at end of file
...@@ -22,6 +22,7 @@ class StudentGradebook(TimeStampedModel): ...@@ -22,6 +22,7 @@ class StudentGradebook(TimeStampedModel):
user = models.ForeignKey(User, db_index=True) user = models.ForeignKey(User, db_index=True)
course_id = CourseKeyField(db_index=True, max_length=255, blank=True) course_id = CourseKeyField(db_index=True, max_length=255, blank=True)
grade = models.FloatField() grade = models.FloatField()
proforma_grade = models.FloatField()
class Meta: class Meta:
""" """
...@@ -121,6 +122,7 @@ class StudentGradebookHistory(TimeStampedModel): ...@@ -121,6 +122,7 @@ class StudentGradebookHistory(TimeStampedModel):
user = models.ForeignKey(User, db_index=True) user = models.ForeignKey(User, db_index=True)
course_id = CourseKeyField(db_index=True, max_length=255, blank=True) course_id = CourseKeyField(db_index=True, max_length=255, blank=True)
grade = models.FloatField() grade = models.FloatField()
proforma_grade = models.FloatField()
@receiver(post_save, sender=StudentGradebook) @receiver(post_save, sender=StudentGradebook)
def save_history(sender, instance, **kwargs): # pylint: disable=no-self-argument, unused-argument def save_history(sender, instance, **kwargs): # pylint: disable=no-self-argument, unused-argument
...@@ -130,6 +132,7 @@ class StudentGradebookHistory(TimeStampedModel): ...@@ -130,6 +132,7 @@ class StudentGradebookHistory(TimeStampedModel):
history_entry = StudentGradebookHistory( history_entry = StudentGradebookHistory(
user=instance.user, user=instance.user,
course_id=instance.course_id, course_id=instance.course_id,
grade=instance.grade grade=instance.grade,
proforma_grade=instance.proforma_grade
) )
history_entry.save() history_entry.save()
...@@ -24,10 +24,12 @@ def on_score_changed(sender, **kwargs): ...@@ -24,10 +24,12 @@ def on_score_changed(sender, **kwargs):
request.user = user request.user = user
grade_data = grades.grade(user, request, course_descriptor) grade_data = grades.grade(user, request, course_descriptor)
grade = grade_data['percent'] grade = grade_data['percent']
proforma_grade = grades.calculate_proforma_grade(grade_data, course_descriptor.grading_policy)
try: try:
gradebook_entry = StudentGradebook.objects.get(user=user, course_id=course_key) gradebook_entry = StudentGradebook.objects.get(user=user, course_id=course_key)
if gradebook_entry.grade != grade: if gradebook_entry.grade != grade:
gradebook_entry.grade = grade gradebook_entry.grade = grade
gradebook_entry.proforma_grade = proforma_grade
gradebook_entry.save() gradebook_entry.save()
except StudentGradebook.DoesNotExist: except StudentGradebook.DoesNotExist:
StudentGradebook.objects.create(user=user, course_id=course_key, grade=grade) StudentGradebook.objects.create(user=user, course_id=course_key, grade=grade, proforma_grade=proforma_grade)
...@@ -8,14 +8,13 @@ from django.db import models ...@@ -8,14 +8,13 @@ from django.db import models
class Migration(SchemaMigration): class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# nothing to do. Simply added a WorkgroupUsers through table.
pass
# Changing field 'WorkgroupSubmission.document_url'
db.alter_column('projects_workgroupsubmission', 'document_url', self.gf('django.db.models.fields.CharField')(max_length=2048))
def backwards(self, orm): def backwards(self, orm):
# nothing to do. Simply added a WorkgroupUsers through table.
# Changing field 'WorkgroupSubmission.document_url' pass
db.alter_column('projects_workgroupsubmission', 'document_url', self.gf('django.db.models.fields.CharField')(max_length=255))
models = { models = {
'api_manager.organization': { 'api_manager.organization': {
...@@ -85,7 +84,7 @@ class Migration(SchemaMigration): ...@@ -85,7 +84,7 @@ class Migration(SchemaMigration):
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'workgroups'", 'to': "orm['projects.Project']"}), 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'workgroups'", 'to': "orm['projects.Project']"}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'workgroups'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}) 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'workgroups'", 'to': "orm['auth.User']", 'through': "orm['projects.WorkgroupUsers']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True'})
}, },
'projects.workgrouppeerreview': { 'projects.workgrouppeerreview': {
'Meta': {'object_name': 'WorkgroupPeerReview'}, 'Meta': {'object_name': 'WorkgroupPeerReview'},
...@@ -132,7 +131,13 @@ class Migration(SchemaMigration): ...@@ -132,7 +131,13 @@ class Migration(SchemaMigration):
'question': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 'question': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'reviewer': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 'reviewer': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'submission': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reviews'", 'to': "orm['projects.WorkgroupSubmission']"}) 'submission': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reviews'", 'to': "orm['projects.WorkgroupSubmission']"})
},
'projects.workgroupusers': {
'Meta': {'object_name': 'WorkgroupUsers', 'db_table': "'projects_workgroup_users'"},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'workgroup': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Workgroup']"})
} }
} }
complete_apps = ['projects'] complete_apps = ['projects']
\ No newline at end of file
...@@ -93,8 +93,8 @@ class WorkgroupReview(TimeStampedModel): ...@@ -93,8 +93,8 @@ class WorkgroupReview(TimeStampedModel):
""" """
workgroup = models.ForeignKey(Workgroup, related_name="workgroup_reviews") workgroup = models.ForeignKey(Workgroup, related_name="workgroup_reviews")
reviewer = models.CharField(max_length=255) # AnonymousUserId reviewer = models.CharField(max_length=255) # AnonymousUserId
question = models.CharField(max_length=255) question = models.CharField(max_length=1024)
answer = models.CharField(max_length=255) answer = models.TextField()
content_id = models.CharField(max_length=255, null=True, blank=True) content_id = models.CharField(max_length=255, null=True, blank=True)
...@@ -121,8 +121,8 @@ class WorkgroupSubmissionReview(TimeStampedModel): ...@@ -121,8 +121,8 @@ class WorkgroupSubmissionReview(TimeStampedModel):
""" """
submission = models.ForeignKey(WorkgroupSubmission, related_name="reviews") submission = models.ForeignKey(WorkgroupSubmission, related_name="reviews")
reviewer = models.CharField(max_length=255) # AnonymousUserId reviewer = models.CharField(max_length=255) # AnonymousUserId
question = models.CharField(max_length=255) question = models.CharField(max_length=1024)
answer = models.CharField(max_length=255) answer = models.TextField()
content_id = models.CharField(max_length=255, null=True, blank=True) content_id = models.CharField(max_length=255, null=True, blank=True)
...@@ -135,6 +135,6 @@ class WorkgroupPeerReview(TimeStampedModel): ...@@ -135,6 +135,6 @@ class WorkgroupPeerReview(TimeStampedModel):
workgroup = models.ForeignKey(Workgroup, related_name="peer_reviews") workgroup = models.ForeignKey(Workgroup, related_name="peer_reviews")
user = models.ForeignKey(User, related_name="workgroup_peer_reviewees") user = models.ForeignKey(User, related_name="workgroup_peer_reviewees")
reviewer = models.CharField(max_length=255) # AnonymousUserId reviewer = models.CharField(max_length=255) # AnonymousUserId
question = models.CharField(max_length=255) question = models.CharField(max_length=1024)
answer = models.CharField(max_length=255) answer = models.TextField()
content_id = models.CharField(max_length=255, null=True, blank=True) content_id = models.CharField(max_length=255, null=True, blank=True)
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