Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
39024a7f
Commit
39024a7f
authored
Dec 10, 2012
by
Don Mitchell
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Grading mostly working
parent
50d7e616
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
542 additions
and
48 deletions
+542
-48
cms/djangoapps/contentstore/tests/test_course_settings.py
+9
-10
cms/djangoapps/contentstore/views.py
+34
-5
cms/djangoapps/models/settings/course_details.py
+5
-3
cms/djangoapps/models/settings/course_grading.py
+239
-0
cms/static/coffee/src/client_templates/course_grade_policy.html
+69
-0
cms/static/js/models/settings/course_details.js
+1
-1
cms/static/js/models/settings/course_grading_policy.js
+116
-0
cms/static/js/models/settings/course_settings.js
+25
-8
cms/static/js/views/settings/main_settings_view.js
+0
-0
cms/static/sass/_settings.scss
+4
-0
cms/templates/settings.html
+0
-0
cms/urls.py
+1
-0
common/djangoapps/util/converters.py
+1
-0
common/lib/xmodule/xmodule/course_module.py
+38
-21
No files found.
cms/djangoapps/contentstore/tests/test_course_settings.py
View file @
39024a7f
...
...
@@ -7,7 +7,7 @@ from django.test.client import Client
from
django.core.urlresolvers
import
reverse
from
xmodule.modulestore
import
Location
from
cms.djangoapps.models.settings.course_details
import
CourseDetails
,
\
Course
Detail
sEncoder
Course
Setting
sEncoder
import
json
from
common.djangoapps.util
import
converters
...
...
@@ -87,7 +87,7 @@ class CourseDetailsTestCase(TestCase):
def
test_encoder
(
self
):
details
=
CourseDetails
.
fetch
(
self
.
course_location
)
jsondetails
=
json
.
dumps
(
details
,
cls
=
Course
Detail
sEncoder
)
jsondetails
=
json
.
dumps
(
details
,
cls
=
Course
Setting
sEncoder
)
jsondetails
=
json
.
loads
(
jsondetails
)
self
.
assertTupleEqual
(
Location
(
jsondetails
[
'course_location'
]),
self
.
course_location
,
"Location !="
)
# Note, start_date is being initialized someplace. I'm not sure why b/c the default will make no sense.
...
...
@@ -164,23 +164,22 @@ class CourseDetailsViewTest(TestCase):
def
alter_field
(
self
,
url
,
details
,
field
,
val
):
details
[
field
]
=
val
jsondetails
=
json
.
dumps
(
details
,
cls
=
Course
Detail
sEncoder
)
jsondetails
=
json
.
dumps
(
details
,
cls
=
Course
Setting
sEncoder
)
resp
=
self
.
client
.
post
(
url
,
jsondetails
)
self
.
assertDictEqual
(
json
.
loads
(
resp
),
details
,
field
+
val
)
self
.
assertDictEqual
(
json
.
loads
(
resp
.
content
),
details
.
__dict__
,
field
+
val
)
def
test_update_and_fetch
(
self
):
details
=
CourseDetails
.
fetch
(
self
.
course_location
)
details_loc
=
self
.
course_location
.
dict
()
.
copy
()
details_loc
[
'section'
]
=
'details'
resp
=
self
.
client
.
get
(
reverse
(
'contentstore.views.get_course_settings'
,
kwargs
=
self
.
course_location
.
dict
()))
resp
=
self
.
client
.
get
(
reverse
(
'course_settings'
,
kwargs
=
{
'org'
:
self
.
course_location
.
org
,
'course'
:
self
.
course_location
.
course
,
'name'
:
self
.
course_location
.
name
}))
self
.
assertContains
(
resp
,
'<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>'
,
status_code
=
200
,
html
=
True
)
# resp s/b json from here on
url
=
reverse
(
'contentstore.views.course_settings_updates'
,
kwargs
=
details_loc
)
url
=
reverse
(
'course_settings'
,
kwargs
=
{
'org'
:
self
.
course_location
.
org
,
'course'
:
self
.
course_location
.
course
,
'name'
:
self
.
course_location
.
name
,
'section'
:
'details'
})
resp
=
self
.
client
.
get
(
url
)
jsondetails
=
json
.
dumps
(
details
,
cls
=
CourseDetailsEncoder
)
self
.
assertDictEqual
(
resp
,
jsondetails
,
"virgin get"
)
self
.
assertDictEqual
(
json
.
loads
(
resp
.
content
),
details
.
__dict__
,
"virgin get"
)
self
.
alter_field
(
url
,
details
,
'start_date'
,
time
.
time
()
*
1000
)
self
.
alter_field
(
url
,
details
,
'start_date'
,
time
.
time
()
*
1000
+
60
*
60
*
24
)
...
...
cms/djangoapps/contentstore/views.py
View file @
39024a7f
...
...
@@ -46,7 +46,8 @@ import time
from
contentstore
import
course_info_model
from
contentstore.utils
import
get_modulestore
from
cms.djangoapps.models.settings.course_details
import
CourseDetails
,
\
CourseDetailsEncoder
CourseSettingsEncoder
from
cms.djangoapps.models.settings.course_grading
import
CourseGradingModel
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
...
...
@@ -955,7 +956,7 @@ def get_course_settings(request, org, course, name):
return
render_to_response
(
'settings.html'
,
{
'active_tab'
:
'settings-tab'
,
'context_course'
:
course_module
,
'course_details'
:
json
.
dumps
(
course_details
,
cls
=
Course
Detail
sEncoder
)
'course_details'
:
json
.
dumps
(
course_details
,
cls
=
Course
Setting
sEncoder
)
})
@expect_json
...
...
@@ -963,7 +964,7 @@ def get_course_settings(request, org, course, name):
@ensure_csrf_cookie
def
course_settings_updates
(
request
,
org
,
course
,
name
,
section
):
"""
restful CRUD operations on course
_info update
s. This differs from get_course_settings by communicating purely
restful CRUD operations on course
setting
s. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
...
...
@@ -971,14 +972,42 @@ def course_settings_updates(request, org, course, name, section):
"""
if
section
==
'details'
:
manager
=
CourseDetails
elif
section
==
'grading'
:
manager
=
CourseGradingModel
else
:
return
if
request
.
method
==
'GET'
:
# Cannot just do a get w/o knowing the course name :-(
return
HttpResponse
(
json
.
dumps
(
manager
.
fetch
(
Location
([
'i4x'
,
org
,
course
,
'course'
,
name
])),
cls
=
CourseDetailsEncoder
),
return
HttpResponse
(
json
.
dumps
(
manager
.
fetch
(
Location
([
'i4x'
,
org
,
course
,
'course'
,
name
])),
cls
=
CourseSettingsEncoder
),
mimetype
=
"application/json"
)
elif
request
.
method
==
'POST'
:
# post or put, doesn't matter.
return
HttpResponse
(
json
.
dumps
(
manager
.
update_from_json
(
request
.
POST
),
cls
=
CourseSettingsEncoder
),
mimetype
=
"application/json"
)
@expect_json
@login_required
@ensure_csrf_cookie
def
course_grader_updates
(
request
,
org
,
course
,
name
,
grader_index
=
None
):
"""
restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
"""
if
request
.
method
==
'POST'
and
'HTTP_X_HTTP_METHOD_OVERRIDE'
in
request
.
META
:
real_method
=
request
.
META
[
'HTTP_X_HTTP_METHOD_OVERRIDE'
]
else
:
real_method
=
request
.
method
if
real_method
==
'GET'
:
# Cannot just do a get w/o knowing the course name :-(
return
HttpResponse
(
json
.
dumps
(
CourseGradingModel
.
fetch_grader
(
Location
([
'i4x'
,
org
,
course
,
'course'
,
name
]),
grader_index
)),
mimetype
=
"application/json"
)
elif
real_method
==
"DELETE"
:
# ??? Shoudl this return anything? Perhaps success fail?
CourseGradingModel
.
delete_grader
(
Location
([
'i4x'
,
org
,
course
,
'course'
,
name
]),
grader_index
)
elif
request
.
method
==
'POST'
:
# post or put, doesn't matter.
return
HttpResponse
(
json
.
dumps
(
manager
.
update_from_json
(
request
.
POST
),
cls
=
CourseDetailsEncoder
),
return
HttpResponse
(
json
.
dumps
(
CourseGradingModel
.
update_grader_from_json
(
Location
([
'i4x'
,
org
,
course
,
'course'
,
name
]),
request
.
POST
)),
mimetype
=
"application/json"
)
...
...
cms/djangoapps/models/settings/course_details.py
View file @
39024a7f
...
...
@@ -6,6 +6,7 @@ from json.encoder import JSONEncoder
import
time
from
contentstore.utils
import
get_modulestore
from
util.converters
import
jsdate_to_time
,
time_to_date
from
cms.djangoapps.models.settings
import
course_grading
class
CourseDetails
:
def
__init__
(
self
,
location
):
...
...
@@ -131,10 +132,11 @@ class CourseDetails:
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
return
CourseDetails
.
fetch
(
course_location
)
class
CourseDetailsEncoder
(
json
.
JSONEncoder
):
# TODO move to a more general util? Is there a better way to do the isinstance model check?
class
CourseSettingsEncoder
(
json
.
JSONEncoder
):
def
default
(
self
,
obj
):
if
isinstance
(
obj
,
CourseDetails
):
if
isinstance
(
obj
,
CourseDetails
)
or
isinstance
(
obj
,
course_grading
.
CourseGradingModel
)
:
return
obj
.
__dict__
elif
isinstance
(
obj
,
Location
):
return
obj
.
dict
()
...
...
cms/djangoapps/models/settings/course_grading.py
0 → 100644
View file @
39024a7f
from
xmodule.modulestore
import
Location
from
contentstore.utils
import
get_modulestore
import
datetime
import
re
from
common.djangoapps.util
import
converters
import
time
class
CourseGradingModel
:
"""
Basically a DAO and Model combo for CRUD operations pertaining to grading policy.
"""
def
__init__
(
self
,
course_descriptor
):
self
.
course_location
=
course_descriptor
.
location
self
.
graders
=
[
CourseGradingModel
.
jsonize_grader
(
i
,
grader
)
for
i
,
grader
in
enumerate
(
course_descriptor
.
raw_grader
)]
# weights transformed to ints [0..100]
self
.
grade_cutoffs
=
course_descriptor
.
grade_cutoffs
self
.
grace_period
=
CourseGradingModel
.
convert_set_grace_period
(
course_descriptor
)
@classmethod
def
fetch
(
cls
,
course_location
):
"""
Fetch the course details for the given course from persistence and return a CourseDetails model.
"""
if
not
isinstance
(
course_location
,
Location
):
course_location
=
Location
(
course_location
)
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
model
=
cls
(
descriptor
)
return
model
@staticmethod
def
fetch_grader
(
course_location
,
index
):
"""
Fetch the course's nth grader
Returns an empty dict if there's no such grader.
"""
if
not
isinstance
(
course_location
,
Location
):
course_location
=
Location
(
course_location
)
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
# # but that would require not using CourseDescriptor's field directly. Opinions?
# FIXME how do I tell it to ignore index? Is there another iteration mech I should use?
if
len
(
descriptor
.
raw_grader
)
>
index
:
return
CourseGradingModel
.
jsonize_grader
(
index
,
descriptor
.
raw_grader
[
index
])
# return empty model
else
:
return
{
"id"
:
index
,
"type"
:
""
,
"min_count"
:
0
,
"drop_count"
:
0
,
"short_label"
:
None
,
"weight"
:
0
}
@staticmethod
def
fetch_cutoffs
(
course_location
):
"""
Fetch the course's grade cutoffs.
"""
if
not
isinstance
(
course_location
,
Location
):
course_location
=
Location
(
course_location
)
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
return
descriptor
.
grade_cutoffs
@staticmethod
def
fetch_grace_period
(
course_location
):
"""
Fetch the course's default grace period.
"""
if
not
isinstance
(
course_location
,
Location
):
course_location
=
Location
(
course_location
)
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
return
{
'grace_period'
:
CourseGradingModel
.
convert_set_grace_period
(
descriptor
)
}
@staticmethod
def
update_from_json
(
jsondict
):
"""
Decode the json into CourseGradingModel and save any changes. Returns the modified model.
Probably not the usual path for updates as it's too coarse grained.
"""
course_location
=
jsondict
[
'course_location'
]
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
graders_parsed
=
[
CourseGradingModel
.
parse_grader
(
jsonele
)
for
jsonele
in
jsondict
[
'graders'
]]
descriptor
.
raw_grader
=
graders_parsed
descriptor
.
grade_cutoffs
=
jsondict
[
'grade_cutoffs'
]
get_modulestore
(
course_location
)
.
update_item
(
course_location
,
descriptor
.
definition
[
'data'
])
CourseGradingModel
.
update_grace_period_from_json
(
course_location
,
jsondict
[
'grace_period'
])
return
CourseGradingModel
.
fetch
(
course_location
)
@staticmethod
def
update_grader_from_json
(
course_location
,
grader
):
"""
Create or update the grader of the given type (string key) for the given course. Returns the modified
grader which is a full model on the client but not on the server (just a dict)
"""
if
not
isinstance
(
course_location
,
Location
):
course_location
=
Location
(
course_location
)
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
# # but that would require not using CourseDescriptor's field directly. Opinions?
# parse removes the id; so, grab it before parse
index
=
grader
.
get
(
'id'
,
None
)
grader
=
CourseGradingModel
.
parse_grader
(
grader
)
if
index
<
len
(
descriptor
.
raw_grader
):
descriptor
.
raw_grader
[
index
]
=
grader
else
:
descriptor
.
raw_grader
.
append
(
grader
)
get_modulestore
(
course_location
)
.
update_item
(
course_location
,
descriptor
.
definition
[
'data'
])
return
grader
@staticmethod
def
update_cutoffs_from_json
(
course_location
,
cutoffs
):
"""
Create or update the grade cutoffs for the given course. Returns sent in cutoffs (ie., no extra
db fetch).
"""
if
not
isinstance
(
course_location
,
Location
):
course_location
=
Location
(
course_location
)
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
descriptor
.
grade_cutoffs
=
cutoffs
get_modulestore
(
course_location
)
.
update_item
(
course_location
,
descriptor
.
definition
[
'data'
])
return
cutoffs
@staticmethod
def
update_grace_period_from_json
(
course_location
,
graceperiodjson
):
"""
Update the course's default grace period.
"""
if
not
isinstance
(
course_location
,
Location
):
course_location
=
Location
(
course_location
)
if
not
isinstance
(
graceperiodjson
,
dict
):
graceperiodjson
=
{
'grace_period'
:
graceperiodjson
}
grace_time
=
converters
.
jsdate_to_time
(
graceperiodjson
[
'grace_period'
])
# NOTE: this does not handle > 24 hours
grace_rep
=
time
.
strftime
(
"
%
H hours
%
M minutes
%
S seconds"
,
grace_time
)
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
descriptor
.
metadata
[
'graceperiod'
]
=
grace_rep
get_modulestore
(
course_location
)
.
update_metadata
(
course_location
,
descriptor
.
metadata
)
return
graceperiodjson
@staticmethod
def
delete_grader
(
course_location
,
index
):
"""
Delete the grader of the given type from the given course.
"""
if
not
isinstance
(
course_location
,
Location
):
course_location
=
Location
(
course_location
)
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
if
index
<
len
(
descriptor
.
raw_grader
):
del
descriptor
.
raw_grader
[
index
]
get_modulestore
(
course_location
)
.
update_item
(
course_location
,
descriptor
.
definition
[
'data'
])
# NOTE cannot delete cutoffs. May be useful to reset
@staticmethod
def
delete_cutoffs
(
course_location
,
cutoffs
):
"""
Resets the cutoffs to the defaults
"""
if
not
isinstance
(
course_location
,
Location
):
course_location
=
Location
(
course_location
)
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
descriptor
.
grade_cutoffs
=
descriptor
.
defaut_grading_policy
[
'GRADE_CUTOFFS'
]
get_modulestore
(
course_location
)
.
update_item
(
course_location
,
descriptor
.
definition
[
'data'
])
return
descriptor
.
grade_cutoffs
@staticmethod
def
delete_grace_period
(
course_location
):
"""
Delete the course's default grace period.
"""
if
not
isinstance
(
course_location
,
Location
):
course_location
=
Location
(
course_location
)
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
del
descriptor
.
metadata
[
'graceperiod'
]
get_modulestore
(
course_location
)
.
update_metadata
(
course_location
,
descriptor
.
metadata
)
@staticmethod
def
convert_set_grace_period
(
descriptor
):
# 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace
=
descriptor
.
metadata
.
get
(
'graceperiod'
,
None
)
if
rawgrace
:
parsedgrace
=
{
str
(
key
):
val
for
(
val
,
key
)
in
re
.
findall
(
'
\
s*(
\
d*)
\
s*(
\
w*)'
,
rawgrace
)}
gracedate
=
datetime
.
datetime
.
today
()
gracedate
=
gracedate
.
replace
(
minute
=
int
(
parsedgrace
.
get
(
'minutes'
,
0
)),
hour
=
int
(
parsedgrace
.
get
(
'hours'
,
0
)))
return
gracedate
.
isoformat
()
+
'Z'
else
:
return
None
@staticmethod
def
parse_grader
(
json_grader
):
# manual to clear out kruft
result
=
{
"type"
:
json_grader
[
"type"
],
"min_count"
:
json_grader
.
get
(
'min_count'
,
0
),
"drop_count"
:
json_grader
.
get
(
'drop_count'
,
0
),
"short_label"
:
json_grader
.
get
(
'short_label'
,
None
),
"weight"
:
json_grader
.
get
(
'weight'
,
0
)
/
100.0
}
return
result
@staticmethod
def
jsonize_grader
(
i
,
grader
):
grader
[
'id'
]
=
i
if
grader
[
'weight'
]:
grader
[
'weight'
]
*=
100
if
not
'short_label'
in
grader
:
grader
[
'short_label'
]
=
""
return
grader
\ No newline at end of file
cms/static/coffee/src/client_templates/course_grade_policy.html
0 → 100644
View file @
39024a7f
<li
class=
"input input-existing multi course-grading-assignment-list-item"
>
<div
class=
"row row-col2"
>
<label
for=
"course-grading-assignment-name"
>
Assignment Type Name:
</label>
<div
class=
"field"
>
<div
class=
"input course-grading-assignment-name"
>
<input
type=
"text"
class=
"long"
id=
"course-grading-assignment-name"
value=
"<%= model.get('type') %>"
>
<span
class=
"tip tip-stacked"
>
e.g. Homework, Labs, Midterm Exams, Final Exam
</span>
</div>
</div>
</div>
<div
class=
"row row-col2"
>
<label
for=
"course-grading-shortname"
>
Abbreviation:
</label>
<div
class=
"field"
>
<div
class=
"input course-grading-shortname"
>
<input
type=
"text"
class=
"short"
id=
"course-grading-assignment-shortname"
value=
"<%= model.get('short_label') %>"
>
<span
class=
"tip tip-inline"
>
e.g. HW, Midterm, Final
</span>
</div>
</div>
</div>
<div
class=
"row row-col2"
>
<label
for=
"course-grading-gradeweight"
>
Weight of Total
Grade:
</label>
<div
class=
"field"
>
<div
class=
"input course-grading-gradeweight"
>
<input
type=
"text"
class=
"short"
id=
"course-grading-assignment-gradeweight"
value =
"<%= model.get('weight') %>"
>
<span
class=
"tip tip-inline"
>
e.g. 25%
</span>
</div>
</div>
</div>
<div
class=
"row row-col2"
>
<label
for=
"course-grading-assignment-totalassignments"
>
Total
Number:
</label>
<div
class=
"field"
>
<div
class=
"input course-grading-totalassignments"
>
<input
type=
"text"
class=
"short"
id=
"course-grading-assignment-totalassignments"
value =
"<%= model.get('min_count') %>"
>
<span
class=
"tip tip-inline"
>
total exercises assigned
</span>
</div>
</div>
</div>
<div
class=
"row row-col2"
>
<label
for=
"course-grading-assignment-droppable"
>
Number of
Droppable:
</label>
<div
class=
"field"
>
<div
class=
"input course-grading-droppable"
>
<input
type=
"text"
class=
"short"
id=
"course-grading-assignment-droppable"
value =
"<%= model.get('drop_count') %>"
>
<span
class=
"tip tip-inline"
>
total exercises that won't be graded
</span>
</div>
</div>
</div>
<a
href=
"#"
class=
"remove-item remove-grading-data"
><span
class=
"delete-icon"
></span>
Delete Assignment Type
</a>
</li>
cms/static/js/models/settings/course_details.js
View file @
39024a7f
...
...
@@ -67,7 +67,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
// NOTE don't return empty errors as that will be interpreted as an error state
},
url
Root
:
function
()
{
url
:
function
()
{
var
location
=
this
.
get
(
'location'
);
return
'/'
+
location
.
get
(
'org'
)
+
"/"
+
location
.
get
(
'course'
)
+
'/settings/'
+
location
.
get
(
'name'
)
+
'/section/details'
;
},
...
...
cms/static/js/models/settings/course_grading_policy.js
0 → 100644
View file @
39024a7f
if
(
!
CMS
.
Models
[
'Settings'
])
CMS
.
Models
.
Settings
=
new
Object
();
CMS
.
Models
.
Settings
.
CourseGradingPolicy
=
Backbone
.
Model
.
extend
({
defaults
:
{
course_location
:
null
,
graders
:
null
,
// CourseGraderCollection
grade_cutoffs
:
null
,
// CourseGradeCutoff model
grace_period
:
null
// either null or seconds of grace period
},
parse
:
function
(
attributes
)
{
if
(
attributes
[
'course_location'
])
{
attributes
.
course_location
=
new
CMS
.
Models
.
Location
(
attributes
.
course_location
,
{
parse
:
true
});
}
if
(
attributes
[
'grace_period'
])
{
attributes
.
grace_period
=
new
Date
(
attributes
.
grace_period
);
}
if
(
attributes
[
'graders'
])
{
var
graderCollection
;
if
(
this
.
has
(
'graders'
))
{
graderCollection
=
this
.
get
(
'graders'
);
graderCollection
.
reset
(
attributes
.
graders
);
}
else
{
graderCollection
=
new
CMS
.
Models
.
Settings
.
CourseGraderCollection
(
attributes
.
graders
);
graderCollection
.
course_location
=
attributes
[
'course_location'
]
||
this
.
get
(
'course_location'
);
}
attributes
.
graders
=
graderCollection
;
}
return
attributes
;
},
url
:
function
()
{
var
location
=
this
.
get
(
'course_location'
);
return
'/'
+
location
.
get
(
'org'
)
+
"/"
+
location
.
get
(
'course'
)
+
'/settings/'
+
location
.
get
(
'name'
)
+
'/section/grading'
;
}
});
CMS
.
Models
.
Settings
.
CourseGrader
=
Backbone
.
Model
.
extend
({
defaults
:
{
"type"
:
""
,
// must be unique w/in collection (ie. w/in course)
"min_count"
:
0
,
"drop_count"
:
0
,
"short_label"
:
""
,
// what to use in place of type if space is an issue
"weight"
:
0
// int 0..100
},
initialize
:
function
()
{
if
(
!
this
.
collection
)
console
.
log
(
"damn"
);
},
parse
:
function
(
attrs
)
{
if
(
attrs
[
'weight'
])
{
if
(
!
_
.
isNumber
(
attrs
.
weight
))
attrs
.
weight
=
parseInt
(
attrs
.
weight
);
}
if
(
attrs
[
'min_count'
])
{
if
(
!
_
.
isNumber
(
attrs
.
min_count
))
attrs
.
min_count
=
parseInt
(
attrs
.
min_count
);
}
if
(
attrs
[
'drop_count'
])
{
if
(
!
_
.
isNumber
(
attrs
.
drop_count
))
attrs
.
drop_count
=
parseInt
(
attrs
.
drop_count
);
}
return
attrs
;
},
validate
:
function
(
attrs
)
{
var
errors
=
{};
if
(
attrs
[
'type'
])
{
if
(
_
.
isEmpty
(
attrs
[
'type'
]))
{
errors
.
type
=
"The assignment type must have a name."
;
}
else
{
// FIXME somehow this.collection is unbound sometimes. I can't track down when
var
existing
=
this
.
collection
&&
this
.
collection
.
some
(
function
(
other
)
{
return
(
other
!=
this
)
&&
(
other
.
get
(
'type'
)
==
attrs
[
'type'
]);},
this
);
if
(
existing
)
{
errors
.
type
=
"There's already another assignment type with this name."
;
}
}
}
if
(
attrs
[
'weight'
])
{
if
(
!
parseInt
(
attrs
.
weight
))
{
errors
.
weight
=
"Please enter an integer between 0 and 100."
;
}
else
{
attrs
.
weight
=
parseInt
(
attrs
.
weight
);
// see if this ensures value saved is int
if
(
this
.
collection
&&
attrs
.
weight
>
0
)
{
// if get() doesn't get the value before the call, use previous()
if
((
this
.
collection
.
sumWeights
()
+
attrs
.
weight
-
this
.
get
(
'weight'
))
>
100
)
errors
.
weight
=
"The weights cannot add to more than 100."
;
}
}}
if
(
attrs
[
'min_count'
])
{
if
(
!
parseInt
(
attrs
.
min_count
))
{
errors
.
min_count
=
"Please enter an integer."
;
}
else
attrs
.
min_count
=
parseInt
(
attrs
.
min_count
);
}
if
(
attrs
[
'drop_count'
])
{
if
(
!
parseInt
(
attrs
.
drop_count
))
{
errors
.
drop_count
=
"Please enter an integer."
;
}
else
attrs
.
drop_count
=
parseInt
(
attrs
.
drop_count
);
}
if
(
attrs
[
'min_count'
]
&&
attrs
[
'drop_count'
]
&&
attrs
.
drop_count
>
attrs
.
min_count
)
{
errors
.
drop_count
=
"Cannot drop more "
+
attrs
.
type
+
" than will assigned."
;
}
if
(
!
_
.
isEmpty
(
errors
))
return
errors
;
}
});
CMS
.
Models
.
Settings
.
CourseGraderCollection
=
Backbone
.
Collection
.
extend
({
model
:
CMS
.
Models
.
Settings
.
CourseGrader
,
course_location
:
null
,
// must be set to a Location object
url
:
function
()
{
return
'/'
+
this
.
course_location
.
get
(
'org'
)
+
"/"
+
this
.
course_location
.
get
(
'course'
)
+
'/grades/'
+
this
.
course_location
.
get
(
'name'
);
},
sumWeights
:
function
()
{
return
this
.
reduce
(
function
(
subtotal
,
grader
)
{
return
subtotal
+
grader
.
get
(
'weight'
);
},
0
);
}
});
\ No newline at end of file
cms/static/js/models/settings/course_settings.js
View file @
39024a7f
...
...
@@ -13,15 +13,31 @@ CMS.Models.Settings.CourseSettings = Backbone.Model.extend({
retrieve
:
function
(
submodel
,
callback
)
{
if
(
this
.
get
(
submodel
))
callback
();
else
switch
(
submodel
)
{
case
'details'
:
this
.
set
(
'details'
,
new
CMS
.
Models
.
Settings
.
CourseDetails
({
location
:
this
.
get
(
'courseLocation'
)})).
fetch
({
success
:
callback
});
break
;
else
{
var
cachethis
=
this
;
switch
(
submodel
)
{
case
'details'
:
var
details
=
new
CMS
.
Models
.
Settings
.
CourseDetails
({
location
:
this
.
get
(
'courseLocation'
)});
details
.
fetch
(
{
success
:
function
(
model
)
{
cachethis
.
set
(
'details'
,
model
);
callback
(
model
);
}
});
break
;
case
'grading'
:
var
grading
=
new
CMS
.
Models
.
Settings
.
CourseGradingPolicy
({
course_location
:
this
.
get
(
'courseLocation'
)});
grading
.
fetch
(
{
success
:
function
(
model
)
{
cachethis
.
set
(
'grading'
,
model
);
callback
(
model
);
}
});
break
;
default
:
break
;
default
:
break
;
}
}
}
})
\ No newline at end of file
cms/static/js/views/settings/main_settings_view.js
View file @
39024a7f
This diff is collapsed.
Click to expand it.
cms/static/sass/_settings.scss
View file @
39024a7f
...
...
@@ -716,6 +716,10 @@
}
}
}
.grade-specific-bar
{
height
:
50px
;
}
.grades
{
position
:
relative
;
...
...
cms/templates/settings.html
View file @
39024a7f
This diff is collapsed.
Click to expand it.
cms/urls.py
View file @
39024a7f
...
...
@@ -38,6 +38,7 @@ urlpatterns = ('',
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$'
,
'contentstore.views.course_info_updates'
,
name
=
'course_info'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)$'
,
'contentstore.views.get_course_settings'
,
name
=
'course_settings'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$'
,
'contentstore.views.course_settings_updates'
,
name
=
'course_settings'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/grades/(?P<name>[^/]+)/(?P<grader_index>.*)$'
,
'contentstore.views.course_grader_updates'
,
name
=
'course_settings'
),
url
(
r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$'
,
'contentstore.views.static_pages'
,
name
=
'static_pages'
),
url
(
r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$'
,
'contentstore.views.edit_static'
,
name
=
'edit_static'
),
...
...
common/djangoapps/util/converters.py
View file @
39024a7f
...
...
@@ -6,6 +6,7 @@ def time_to_date(time_obj):
"""
Convert a time.time_struct to a true universal time (can pass to js Date constructor)
"""
# TODO change to using the isoformat() function on datetime. js date can parse those
return
calendar
.
timegm
(
time_obj
)
*
1000
def
jsdate_to_time
(
field
):
...
...
common/lib/xmodule/xmodule/course_module.py
View file @
39024a7f
...
...
@@ -10,6 +10,7 @@ import json
import
logging
import
requests
import
time
import
copy
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -99,19 +100,11 @@ class CourseDescriptor(SequenceDescriptor):
self
.
set_grading_policy
(
self
.
definition
[
'data'
]
.
get
(
'grading_policy'
,
None
))
def
set_grading_policy
(
self
,
course_policy
):
if
course_policy
is
None
:
course_policy
=
{}
def
defaut_grading_policy
(
self
):
"""
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is
missing, it reverts to the default.
Return a dict which is a copy of the default grading policy
"""
default_policy_string
=
"""
{
"GRADER" : [
default
=
{
"GRADER"
:
[
{
"type"
:
"Homework"
,
"min_count"
:
12
,
...
...
@@ -127,33 +120,41 @@ class CourseDescriptor(SequenceDescriptor):
"weight"
:
0.15
},
{
"type" : "Midterm",
"name" : "Midterm Exam",
"type"
:
"Midterm Exam"
,
"short_label"
:
"Midterm"
,
"min_count"
:
1
,
"drop_count"
:
0
,
"weight"
:
0.3
},
{
"type" : "Final",
"name" : "Final Exam",
"type"
:
"Final Exam"
,
"short_label"
:
"Final"
,
"min_count"
:
1
,
"drop_count"
:
0
,
"weight"
:
0.4
}
],
"GRADE_CUTOFFS"
:
{
"
A" : 0.87,
"B" : 0.7,
"C" : 0.6
}
}
"
Pass"
:
0.5
}}
return
copy
.
deepcopy
(
default
)
def
set_grading_policy
(
self
,
course_policy
):
"""
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is
missing, it reverts to the default.
"""
if
course_policy
is
None
:
course_policy
=
{}
# Load the global settings as a dictionary
grading_policy
=
json
.
loads
(
default_policy_string
)
grading_policy
=
self
.
defaut_grading_policy
(
)
# Override any global settings with the course settings
grading_policy
.
update
(
course_policy
)
# Here is where we should parse any configurations, so that we can fail early
grading_policy
[
'RAW_GRADER'
]
=
grading_policy
[
'GRADER'
]
# used for cms access
grading_policy
[
'GRADER'
]
=
grader_from_conf
(
grading_policy
[
'GRADER'
])
self
.
_grading_policy
=
grading_policy
...
...
@@ -272,10 +273,26 @@ class CourseDescriptor(SequenceDescriptor):
@property
def
grader
(
self
):
return
self
.
_grading_policy
[
'GRADER'
]
@property
def
raw_grader
(
self
):
return
self
.
_grading_policy
[
'RAW_GRADER'
]
@raw_grader.setter
def
raw_grader
(
self
,
value
):
# NOTE WELL: this change will not update the processed graders. If we need that, this needs to call grader_from_conf
self
.
_grading_policy
[
'RAW_GRADER'
]
=
value
self
.
definition
[
'data'
]
.
setdefault
(
'grading_policy'
,{})[
'GRADER'
]
=
value
@property
def
grade_cutoffs
(
self
):
return
self
.
_grading_policy
[
'GRADE_CUTOFFS'
]
@grade_cutoffs.setter
def
grade_cutoffs
(
self
,
value
):
self
.
_grading_policy
[
'GRADE_CUTOFFS'
]
=
value
self
.
definition
[
'data'
]
.
setdefault
(
'grading_policy'
,{})[
'GRADE_CUTOFFS'
]
=
value
@property
def
tabs
(
self
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment