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
8f16d639
Commit
8f16d639
authored
Feb 05, 2013
by
Don Mitchell
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
CRUD on policy fields w/ some validation
parent
07f87d47
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
967 additions
and
822 deletions
+967
-822
cms/djangoapps/contentstore/views.py
+12
-10
cms/djangoapps/models/settings/course_metadata.py
+6
-10
cms/static/client_templates/advanced_entry.html
+19
-0
cms/static/js/models/settings/advanced.js
+203
-5
cms/static/js/models/settings/course_grading_policy.js
+107
-106
cms/static/js/template_loader.js
+1
-1
cms/static/js/views/settings/main_settings_view.js
+609
-593
cms/templates/settings.html
+10
-97
No files found.
cms/djangoapps/contentstore/views.py
View file @
8f16d639
...
...
@@ -1109,6 +1109,8 @@ def get_course_settings(request, org, course, name):
return
render_to_response
(
'settings.html'
,
{
'active_tab'
:
'settings'
,
'context_course'
:
course_module
,
'advanced_blacklist'
:
json
.
dumps
(
CourseMetadata
.
FILTERED_LIST
),
'advanced_dict'
:
json
.
dumps
(
CourseMetadata
.
fetch
(
location
)),
'course_details'
:
json
.
dumps
(
course_details
,
cls
=
CourseSettingsEncoder
)
})
...
...
@@ -1133,6 +1135,9 @@ def course_settings_updates(request, org, course, name, section):
manager
=
CourseDetails
elif
section
==
'grading'
:
manager
=
CourseGradingModel
elif
section
==
'advanced'
:
# not implemented b/c it assumes prefetched and then everything thru course_edit_metadata
return
else
:
return
if
request
.
method
==
'GET'
:
...
...
@@ -1194,14 +1199,10 @@ def course_edit_metadata(request, org, course, name):
editable
=
CourseMetadata
.
fetch
(
location
)
return
render_to_response
(
'course_info.html'
,
{
'active_tab'
:
'settings'
,
'editable_metadata'
:
editable
,
'url_base'
:
"/"
+
org
+
"/"
+
course
+
"/"
,
'blacklist_keys'
:
CourseMetadata
.
FILTERED_LIST
})
# for now defer to settings general until we split the divs out into separate pages
return
get_course_settings
(
request
,
org
,
course
,
name
)
@expect_json
## NB: expect_json failed on ["key", "key2"] and json payload
@login_required
@ensure_csrf_cookie
def
course_metadata_rest_access
(
request
,
org
,
course
,
name
):
...
...
@@ -1225,10 +1226,11 @@ def course_metadata_rest_access(request, org, course, name):
if
request
.
method
==
'GET'
:
return
HttpResponse
(
json
.
dumps
(
CourseMetadata
.
fetch
(
location
)),
mimetype
=
"application/json"
)
elif
real_method
==
'DELETE'
:
# coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
return
HttpResponse
(
json
.
dumps
(
CourseMetadata
.
delete_key
(
location
,
request
.
POST
)),
mimetype
=
"application/json"
)
elif
real_method
==
'DELETE'
:
return
HttpResponse
(
json
.
dumps
(
CourseMetadata
.
delete_key
(
location
,
json
.
loads
(
request
.
body
)
)),
mimetype
=
"application/json"
)
elif
request
.
method
==
'POST'
:
return
HttpResponse
(
json
.
dumps
(
CourseMetadata
.
update_from_json
(
location
,
request
.
POST
)),
mimetype
=
"application/json"
)
# NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
return
HttpResponse
(
json
.
dumps
(
CourseMetadata
.
update_from_json
(
location
,
json
.
loads
(
request
.
body
))),
mimetype
=
"application/json"
)
@login_required
...
...
cms/djangoapps/models/settings/course_metadata.py
View file @
8f16d639
...
...
@@ -7,8 +7,8 @@ class CourseMetadata(object):
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones.
The objects have no predefined attrs but instead are obj encodings of the editable metadata.
'''
FILTERED_LIST
=
[
'start'
,
'end'
,
'enrollment_start'
,
'enrollment_end'
,
'tabs'
,
'graceperiod'
]
# __new_advanced_key__ is used by client not server; so, could argue against it being here
FILTERED_LIST
=
[
'start'
,
'end'
,
'enrollment_start'
,
'enrollment_end'
,
'tabs'
,
'graceperiod'
,
'__new_advanced_key__'
]
@classmethod
def
fetch
(
cls
,
course_location
):
...
...
@@ -57,14 +57,10 @@ class CourseMetadata(object):
'''
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
if
isinstance
(
payload
,
list
):
for
key
in
payload
:
if
key
in
descriptor
.
metadata
:
del
descriptor
.
metadata
[
key
]
else
:
if
payload
in
descriptor
.
metadata
:
del
descriptor
.
metadata
[
payload
]
for
key
in
payload
[
'deleteKeys'
]:
if
key
in
descriptor
.
metadata
:
del
descriptor
.
metadata
[
key
]
get_modulestore
(
course_location
)
.
update_metadata
(
course_location
,
descriptor
.
metadata
)
return
cls
.
fetch
(
course_location
)
cms/static/client_templates/advanced_entry.html
0 → 100644
View file @
8f16d639
<li
class=
"input multi course-advanced-policy-list-item"
>
<div
class=
"row"
>
<div
class=
"key"
id=
"<%= (_.isEmpty(key) ? '__new_advanced_key__' : key) %>"
>
<label
for=
"course-advanced-policy-key"
>
Policy Key:
</label>
<div
class=
"field"
>
<input
type=
"text"
class=
"short"
id=
"course-advanced-policy-key"
value=
"<%= key %>"
/>
<span
class=
"tip tip-stacked"
>
Keys are case sensitive and cannot contain spaces or start with a number
</span>
</div>
</div>
<div
class=
"value"
>
<label
for=
"course-advanced-policy-value"
>
Policy Value:
</label>
<div
class=
"field"
>
<textarea
class=
"ace text"
id=
"course-advanced-policy-value"
><
%=
value
%
></textarea>
</div>
</div>
</div>
<a
href=
"#"
class=
"delete-button standard remove-item advanced-policy-data"
>
<span
class=
"delete-icon"
></span>
Delete
</a>
</li>
\ No newline at end of file
cms/static/js/models/settings/advanced.js
View file @
8f16d639
...
...
@@ -2,13 +2,211 @@ if (!CMS.Models['Settings']) CMS.Models.Settings = {};
CMS
.
Models
.
Settings
.
Advanced
=
Backbone
.
Model
.
extend
({
defaults
:
{
// the properties are whatever the user types in
},
// which keys to send as the deleted keys on next save
deleteKeys
:
[],
blacklistKeys
:
[],
// an array which the controller should populate directly for now [static not instance based]
initialize
:
function
()
{
console
.
log
(
'in initialize'
);
var
editor
=
ace
.
edit
(
'course-advanced-policy-1-value'
);
editor
.
setTheme
(
"ace/theme/monokai"
);
editor
.
getSession
().
setMode
(
"ace/mode/javascript"
);
},
validate
:
function
(
attrs
)
{
var
errors
=
{};
for
(
key
in
attrs
)
{
if
(
_
.
contains
(
this
.
blacklistKeys
,
key
))
{
errors
[
key
]
=
key
+
" is a reserved keyword or has another editor"
;
}
}
if
(
!
_
.
isEmpty
(
errors
))
return
errors
;
}
});
if
(
!
CMS
.
Views
[
'Settings'
])
CMS
.
Views
.
Settings
=
{};
CMS
.
Views
.
Settings
.
Advanced
=
CMS
.
Views
.
ValidatingView
.
extend
({
// Model class is CMS.Models.Settings.Advanced
events
:
{
'click .delete-button'
:
"deleteEntry"
,
'click .save-button'
:
"saveView"
,
'click .cancel-button'
:
"revertView"
,
'click .new-button'
:
"addEntry"
,
// update model on changes
'change #course-advanced-policy-key'
:
"updateKey"
,
'change #course-advanced-policy-value'
:
"updateValue"
// TODO enable/disable save (add disabled class) based on validation & dirty
// TODO enable/disable new button?
},
initialize
:
function
()
{
var
self
=
this
;
// instantiates an editor template for each update in the collection
window
.
templateLoader
.
loadRemoteTemplate
(
"advanced_entry"
,
"/static/client_templates/advanced_entry.html"
,
function
(
raw_template
)
{
self
.
template
=
_
.
template
(
raw_template
);
self
.
render
();
}
);
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
},
render
:
function
()
{
// catch potential outside call before template loaded
if
(
!
this
.
template
)
return
this
;
var
listEle$
=
this
.
$el
.
find
(
'.course-advanced-policy-list'
);
listEle$
.
empty
();
// same object so manipulations to one keep the other up to date
this
.
fieldToSelectorMap
=
this
.
selectorToField
=
{};
// iterate through model and produce key : value editors for each property in model.get
var
self
=
this
;
_
.
each
(
this
.
model
.
attributes
,
function
(
value
,
key
)
{
listEle$
.
append
(
self
.
template
({
key
:
key
,
value
:
value
}));
self
.
fieldToSelectorMap
[
key
]
=
key
;
});
// insert the empty one
this
.
addEntry
();
// Should this be on an event rather than render?
// var editor = ace.edit('course-advanced-policy-1-value');
// editor.setTheme("ace/theme/monokai");
// editor.getSession().setMode("ace/mode/javascript");
return
this
;
},
deleteEntry
:
function
(
event
)
{
event
.
preventDefault
();
// find out which entry
var
li$
=
$
(
event
.
currentTarget
).
closest
(
'li'
);
// Not data b/c the validation view uses it for a selector
var
key
=
$
(
'.key'
,
li$
).
attr
(
'id'
);
delete
this
.
fieldToSelectorMap
[
key
];
if
(
key
!==
'__new_advanced_key__'
)
{
this
.
model
.
deleteKeys
.
push
(
key
);
delete
this
.
model
[
key
];
}
li$
.
remove
();
},
saveView
:
function
(
event
)
{
// TODO one last verification scan:
// call validateKey on each to ensure proper format
// check for dupes
this
.
model
.
save
({
success
:
function
()
{
window
.
alert
(
"Saved"
);
},
error
:
CMS
.
ServerError
});
// FIXME don't delete if the validation didn't succeed in the save call
// remove deleted attrs
if
(
!
_
.
isEmpty
(
this
.
model
.
deleteKeys
))
{
var
self
=
this
;
// hmmm, not sure how to do this via backbone since we're not destroying the model
$
.
ajax
({
url
:
this
.
model
.
url
,
// json to and fro
contentType
:
"application/json"
,
dataType
:
"json"
,
// delete
type
:
'DELETE'
,
// data
data
:
JSON
.
stringify
({
deleteKeys
:
this
.
model
.
deleteKeys
})
})
.
fail
(
function
(
hdr
,
status
,
error
)
{
CMS
.
ServerError
(
self
.
model
,
"Deleting keys:"
+
status
);
})
.
done
(
function
(
data
,
status
,
error
)
{
// clear deleteKeys on success
self
.
model
.
deleteKeys
=
[];
});
}
},
revertView
:
function
(
event
)
{
this
.
model
.
deleteKeys
=
[];
var
self
=
this
;
this
.
model
.
fetch
({
success
:
this
.
render
,
error
:
CMS
.
ServerError
});
},
addEntry
:
function
()
{
var
listEle$
=
this
.
$el
.
find
(
'.course-advanced-policy-list'
);
listEle$
.
append
(
this
.
template
({
key
:
""
,
value
:
""
}));
// disable the value entry until there's an acceptable key
listEle$
.
find
(
'.course-advanced-policy-value'
).
addClass
(
'disabled'
);
this
.
fieldToSelectorMap
[
'__new_advanced_key__'
]
=
'__new_advanced_key__'
;
},
updateKey
:
function
(
event
)
{
// old key: either the key as in the model or __new_advanced_key__. That is, it doesn't change as the val changes until val is accepted
var
oldKey
=
$
(
event
.
currentTarget
).
closest
(
'.key'
).
attr
(
'id'
);
var
newKey
=
$
(
event
.
currentTarget
).
val
();
console
.
log
(
'update '
,
oldKey
,
newKey
);
// REMOVE ME
if
(
oldKey
!==
newKey
)
{
// may erase other errors but difficult to just remove these
this
.
clearValidationErrors
();
if
(
!
this
.
validateKey
(
oldKey
,
newKey
))
return
;
if
(
this
.
model
.
has
(
newKey
))
{
console
.
log
(
'dupe key'
);
var
error
=
{};
error
[
oldKey
]
=
newKey
+
" has another entry"
;
error
[
newKey
]
=
"Other entry for "
+
newKey
;
this
.
model
.
trigger
(
"error"
,
this
.
model
,
error
);
return
false
;
}
// explicitly call validate to determine whether to proceed (relying on triggered error means putting continuation in the success
// method which is uglier I think?)
var
newEntryModel
=
{};
// set the new key's value to the old one's
newEntryModel
[
newKey
]
=
(
oldKey
===
'__new_advanced_key__'
?
''
:
this
.
model
.
get
(
oldKey
));
var
validation
=
this
.
model
.
validate
(
newEntryModel
);
if
(
validation
)
{
console
.
log
(
'reserved key'
);
this
.
model
.
trigger
(
"error"
,
this
.
model
,
validation
);
// abandon update
return
;
}
// Now safe to actually do the update
this
.
model
.
set
(
newEntryModel
);
delete
this
.
fieldToSelectorMap
[
oldKey
];
if
(
oldKey
!==
'__new_advanced_key__'
)
{
// mark the old key for deletion and delete from field maps
this
.
model
.
deleteKeys
.
push
(
oldKey
);
this
.
model
.
unset
(
oldKey
)
;
}
else
{
// enable the value entry
this
.
$el
.
find
(
'.course-advanced-policy-value'
).
removeClass
(
'disabled'
);
}
// update gui (sets all the ids etc)
$
(
event
.
currentTarget
).
closest
(
'li'
).
replaceWith
(
this
.
template
({
key
:
newKey
,
value
:
this
.
model
.
get
(
newKey
)
}));
this
.
fieldToSelectorMap
[
newKey
]
=
newKey
;
}
},
validateKey
:
function
(
oldKey
,
newKey
)
{
// model validation can't handle malformed keys nor notice if 2 fields have same key; so, need to add that chk here
// TODO ensure there's no spaces or illegal chars
if
(
_
.
isEmpty
(
newKey
))
{
console
.
log
(
'no key'
);
var
error
=
{};
error
[
oldKey
]
=
"Key cannot be an empty string"
;
this
.
model
.
trigger
(
"error"
,
this
.
model
,
error
);
return
false
;
}
else
return
true
;
},
updateValue
:
function
(
event
)
{
// much simpler than key munging. just update the value
var
key
=
$
(
event
.
currentTarget
).
closest
(
'.row'
).
children
(
'.key'
).
attr
(
'id'
);
var
value
=
$
(
event
.
currentTarget
).
val
();
console
.
log
(
'updating '
,
key
,
value
);
this
.
model
.
set
(
key
,
value
,
{
validate
:
true
});
}
});
cms/static/js/models/settings/course_grading_policy.js
View file @
8f16d639
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
defaults
:
{
course_location
:
null
,
graders
:
null
,
// CourseGraderCollection
grade_cutoffs
:
null
,
// CourseGradeCutoff model
grace_period
:
null
// either null or { hours: n, minutes: m, ...}
},
parse
:
function
(
attributes
)
{
if
(
attributes
[
'course_location'
])
{
attributes
.
course_location
=
new
CMS
.
Models
.
Location
(
attributes
.
course_location
,
{
parse
:
true
});
}
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'
;
},
gracePeriodToDate
:
function
()
{
var
newDate
=
new
Date
();
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'hours'
])
newDate
.
setHours
(
this
.
get
(
'grace_period'
)[
'hours'
]);
else
newDate
.
setHours
(
0
);
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'minutes'
])
newDate
.
setMinutes
(
this
.
get
(
'grace_period'
)[
'minutes'
]);
else
newDate
.
setMinutes
(
0
);
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'seconds'
])
newDate
.
setSeconds
(
this
.
get
(
'grace_period'
)[
'seconds'
]);
else
newDate
.
setSeconds
(
0
);
return
newDate
;
},
dateToGracePeriod
:
function
(
date
)
{
return
{
hours
:
date
.
getHours
(),
minutes
:
date
.
getMinutes
(),
seconds
:
date
.
getSeconds
()
};
}
},
parse
:
function
(
attributes
)
{
if
(
attributes
[
'course_location'
])
{
attributes
.
course_location
=
new
CMS
.
Models
.
Location
(
attributes
.
course_location
,
{
parse
:
true
});
}
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'
;
},
gracePeriodToDate
:
function
()
{
var
newDate
=
new
Date
();
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'hours'
])
newDate
.
setHours
(
this
.
get
(
'grace_period'
)[
'hours'
]);
else
newDate
.
setHours
(
0
);
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'minutes'
])
newDate
.
setMinutes
(
this
.
get
(
'grace_period'
)[
'minutes'
]);
else
newDate
.
setMinutes
(
0
);
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'seconds'
])
newDate
.
setSeconds
(
this
.
get
(
'grace_period'
)[
'seconds'
]);
else
newDate
.
setSeconds
(
0
);
return
newDate
;
},
dateToGracePeriod
:
function
(
date
)
{
return
{
hours
:
date
.
getHours
(),
minutes
:
date
.
getMinutes
(),
seconds
:
date
.
getSeconds
()
};
}
});
CMS
.
Models
.
Settings
.
CourseGrader
=
Backbone
.
Model
.
extend
({
defaults
:
{
defaults
:
{
"type"
:
""
,
// must be unique w/in collection (ie. w/in course)
"min_count"
:
1
,
"drop_count"
:
0
,
...
...
@@ -57,71 +57,71 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
"weight"
:
0
// int 0..100
},
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
;
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
(
!
isFinite
(
attrs
.
weight
)
||
/
\D
+/
.
test
(
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
)
{
// FIXME b/c saves don't update the models if validation fails, we should
// either revert the field value to the one in the model and make them make room
// or figure out a wholistic way to balance the vals across the whole
//
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
(
!
isFinite
(
attrs
.
min_count
)
||
/
\D
+/
.
test
(
attrs
.
min_count
))
{
errors
.
min_count
=
"Please enter an integer."
;
}
else
attrs
.
min_count
=
parseInt
(
attrs
.
min_count
);
}
if
(
attrs
[
'drop_count'
])
{
if
(
!
isFinite
(
attrs
.
drop_count
)
||
/
\D
+/
.
test
(
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
;
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
(
!
isFinite
(
attrs
.
weight
)
||
/
\D
+/
.
test
(
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
)
{
// FIXME b/c saves don't update the models if validation fails, we should
// either revert the field value to the one in the model and make them make room
// or figure out a wholistic way to balance the vals across the whole
//
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
(
!
isFinite
(
attrs
.
min_count
)
||
/
\D
+/
.
test
(
attrs
.
min_count
))
{
errors
.
min_count
=
"Please enter an integer."
;
}
else
attrs
.
min_count
=
parseInt
(
attrs
.
min_count
);
}
if
(
attrs
[
'drop_count'
])
{
if
(
!
isFinite
(
attrs
.
drop_count
)
||
/
\D
+/
.
test
(
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
);
}
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/template_loader.js
View file @
8f16d639
...
...
@@ -5,7 +5,7 @@
if
(
typeof
window
.
templateLoader
==
'function'
)
return
;
var
templateLoader
=
{
templateVersion
:
"0.0.1
2
"
,
templateVersion
:
"0.0.1
3
"
,
templates
:
{},
loadRemoteTemplate
:
function
(
templateName
,
filename
,
callback
)
{
if
(
!
this
.
templates
[
templateName
])
{
...
...
cms/static/js/views/settings/main_settings_view.js
View file @
8f16d639
if
(
!
CMS
.
Views
[
'Settings'
])
CMS
.
Views
.
Settings
=
{};
//
TODO move to common place
//TODO move to common place
CMS
.
Views
.
ValidatingView
=
Backbone
.
View
.
extend
({
// Intended as an abstract class which catches validation errors on the model and
// decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents
initialize
:
function
()
{
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
},
errorTemplate
:
_
.
template
(
'<span class="message-error"><%= message %></span>'
),
events
:
{
"blur input"
:
"clearValidationErrors"
,
"blur textarea"
:
"clearValidationErrors"
},
fieldToSelectorMap
:
{
// Your subclass must populate this w/ all of the model keys and dom selectors
// which may be the subjects of validation errors
},
_cacheValidationErrors
:
[],
handleValidationError
:
function
(
model
,
error
)
{
// error is object w/ fields and error strings
for
(
var
field
in
error
)
{
var
ele
=
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
field
]);
this
.
_cacheValidationErrors
.
push
(
ele
);
if
(
$
(
ele
).
is
(
'div'
))
{
// put error on the contained inputs
$
(
ele
).
find
(
'input, textarea'
).
addClass
(
'error'
);
}
else
$
(
ele
).
addClass
(
'error'
);
$
(
ele
).
parent
().
append
(
this
.
errorTemplate
({
message
:
error
[
field
]}));
}
},
clearValidationErrors
:
function
()
{
// error is object w/ fields and error strings
while
(
this
.
_cacheValidationErrors
.
length
>
0
)
{
var
ele
=
this
.
_cacheValidationErrors
.
pop
();
if
(
$
(
ele
).
is
(
'div'
))
{
// put error on the contained inputs
$
(
ele
).
find
(
'input, textarea'
).
removeClass
(
'error'
);
}
else
$
(
ele
).
removeClass
(
'error'
);
$
(
ele
).
nextAll
(
'.message-error'
).
remove
();
}
},
saveIfChanged
:
function
(
event
)
{
// returns true if the value changed and was thus sent to server
var
field
=
this
.
selectorToField
[
event
.
currentTarget
.
id
];
var
currentVal
=
this
.
model
.
get
(
field
);
var
newVal
=
$
(
event
.
currentTarget
).
val
();
if
(
currentVal
!=
newVal
)
{
this
.
clearValidationErrors
();
this
.
model
.
save
(
field
,
newVal
,
{
error
:
CMS
.
ServerError
});
return
true
;
}
else
return
false
;
}
// Intended as an abstract class which catches validation errors on the model and
// decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents
initialize
:
function
()
{
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
},
errorTemplate
:
_
.
template
(
'<span class="message-error"><%= message %></span>'
),
events
:
{
"change input"
:
"clearValidationErrors"
,
"change textarea"
:
"clearValidationErrors"
},
fieldToSelectorMap
:
{
// Your subclass must populate this w/ all of the model keys and dom selectors
// which may be the subjects of validation errors
},
_cacheValidationErrors
:
[],
handleValidationError
:
function
(
model
,
error
)
{
console
.
log
(
'validation'
,
model
,
error
);
// error is object w/ fields and error strings
for
(
var
field
in
error
)
{
var
ele
=
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
field
]);
this
.
_cacheValidationErrors
.
push
(
ele
);
if
(
$
(
ele
).
is
(
'div'
))
{
// put error on the contained inputs
$
(
ele
).
find
(
'input, textarea'
).
addClass
(
'error'
);
}
else
$
(
ele
).
addClass
(
'error'
);
$
(
ele
).
parent
().
append
(
this
.
errorTemplate
({
message
:
error
[
field
]}));
}
},
clearValidationErrors
:
function
()
{
// error is object w/ fields and error strings
while
(
this
.
_cacheValidationErrors
.
length
>
0
)
{
var
ele
=
this
.
_cacheValidationErrors
.
pop
();
if
(
$
(
ele
).
is
(
'div'
))
{
// put error on the contained inputs
$
(
ele
).
find
(
'input, textarea'
).
removeClass
(
'error'
);
}
else
$
(
ele
).
removeClass
(
'error'
);
$
(
ele
).
nextAll
(
'.message-error'
).
remove
();
}
},
saveIfChanged
:
function
(
event
)
{
// returns true if the value changed and was thus sent to server
var
field
=
this
.
selectorToField
[
event
.
currentTarget
.
id
];
var
currentVal
=
this
.
model
.
get
(
field
);
var
newVal
=
$
(
event
.
currentTarget
).
val
();
if
(
currentVal
!=
newVal
)
{
this
.
clearValidationErrors
();
this
.
model
.
save
(
field
,
newVal
,
{
error
:
CMS
.
ServerError
});
return
true
;
}
else
return
false
;
}
});
CMS
.
Views
.
Settings
.
Main
=
Backbone
.
View
.
extend
({
// Model class is CMS.Models.Settings.CourseSettings
// allow navigation between the tabs
events
:
{
'click .settings-page-menu a'
:
"showSettingsTab"
,
'mouseover #timezone'
:
"updateTime"
},
currentTab
:
null
,
subviews
:
{},
// indexed by tab name
initialize
:
function
()
{
// load templates
this
.
currentTab
=
this
.
$el
.
find
(
'.settings-page-menu .is-shown'
).
attr
(
'data-section'
);
// create the initial subview
this
.
subviews
[
this
.
currentTab
]
=
this
.
createSubview
();
// fill in fields
this
.
$el
.
find
(
"#course-name"
).
val
(
this
.
model
.
get
(
'courseLocation'
).
get
(
'name'
));
this
.
$el
.
find
(
"#course-organization"
).
val
(
this
.
model
.
get
(
'courseLocation'
).
get
(
'org'
));
this
.
$el
.
find
(
"#course-number"
).
val
(
this
.
model
.
get
(
'courseLocation'
).
get
(
'course'
));
this
.
$el
.
find
(
'.set-date'
).
datepicker
({
'dateFormat'
:
'm/d/yy'
});
this
.
$el
.
find
(
":input, textarea"
).
focus
(
function
()
{
$
(
"label[for='"
+
this
.
id
+
"']"
).
addClass
(
"is-focused"
);
}).
blur
(
function
()
{
$
(
"label"
).
removeClass
(
"is-focused"
);
});
this
.
render
();
},
render
:
function
()
{
// create any necessary subviews and put them onto the page
if
(
!
this
.
model
.
has
(
this
.
currentTab
))
{
// TODO disable screen until fetch completes?
var
cachethis
=
this
;
this
.
model
.
retrieve
(
this
.
currentTab
,
function
()
{
cachethis
.
subviews
[
cachethis
.
currentTab
]
=
cachethis
.
createSubview
();
cachethis
.
subviews
[
cachethis
.
currentTab
].
render
();
});
}
else
this
.
subviews
[
this
.
currentTab
].
render
();
var
dateIntrospect
=
new
Date
();
this
.
$el
.
find
(
'#timezone'
).
html
(
"("
+
dateIntrospect
.
getTimezone
()
+
")"
);
return
this
;
},
createSubview
:
function
()
{
switch
(
this
.
currentTab
)
{
case
'details'
:
return
new
CMS
.
Views
.
Settings
.
Details
({
el
:
this
.
$el
.
find
(
'.settings-'
+
this
.
currentTab
),
model
:
this
.
model
.
get
(
this
.
currentTab
)
});
case
'faculty'
:
break
;
case
'grading'
:
return
new
CMS
.
Views
.
Settings
.
Grading
({
el
:
this
.
$el
.
find
(
'.settings-'
+
this
.
currentTab
),
model
:
this
.
model
.
get
(
this
.
currentTab
)
});
case
'problems'
:
break
;
case
'discussions'
:
break
;
}
},
updateTime
:
function
(
e
)
{
var
now
=
new
Date
();
var
hours
=
now
.
getHours
();
var
minutes
=
now
.
getMinutes
();
$
(
e
.
currentTarget
).
attr
(
'title'
,
(
hours
%
12
===
0
?
12
:
hours
%
12
)
+
":"
+
(
minutes
<
10
?
"0"
:
""
)
+
now
.
getMinutes
()
+
(
hours
<
12
?
"am"
:
"pm"
)
+
" (current local time)"
);
},
showSettingsTab
:
function
(
e
)
{
this
.
currentTab
=
$
(
e
.
target
).
attr
(
'data-section'
);
$
(
'.settings-page-section > section'
).
hide
();
$
(
'.settings-'
+
this
.
currentTab
).
show
();
$
(
'.settings-page-menu .is-shown'
).
removeClass
(
'is-shown'
);
$
(
e
.
target
).
addClass
(
'is-shown'
);
// fetch model for the tab if not loaded already
this
.
render
();
}
// Model class is CMS.Models.Settings.CourseSettings
// allow navigation between the tabs
events
:
{
'click .settings-page-menu a'
:
"showSettingsTab"
,
'mouseover #timezone'
:
"updateTime"
},
currentTab
:
null
,
subviews
:
{},
// indexed by tab name
initialize
:
function
()
{
// load templates
this
.
currentTab
=
this
.
$el
.
find
(
'.settings-page-menu .is-shown'
).
attr
(
'data-section'
);
// create the initial subview
this
.
subviews
[
this
.
currentTab
]
=
this
.
createSubview
();
// fill in fields
this
.
$el
.
find
(
"#course-name"
).
val
(
this
.
model
.
get
(
'courseLocation'
).
get
(
'name'
));
this
.
$el
.
find
(
"#course-organization"
).
val
(
this
.
model
.
get
(
'courseLocation'
).
get
(
'org'
));
this
.
$el
.
find
(
"#course-number"
).
val
(
this
.
model
.
get
(
'courseLocation'
).
get
(
'course'
));
this
.
$el
.
find
(
'.set-date'
).
datepicker
({
'dateFormat'
:
'm/d/yy'
});
this
.
$el
.
find
(
":input, textarea"
).
focus
(
function
()
{
$
(
"label[for='"
+
this
.
id
+
"']"
).
addClass
(
"is-focused"
);
}).
blur
(
function
()
{
$
(
"label"
).
removeClass
(
"is-focused"
);
});
this
.
render
();
},
render
:
function
()
{
// create any necessary subviews and put them onto the page
if
(
!
this
.
model
.
has
(
this
.
currentTab
))
{
// TODO disable screen until fetch completes?
var
cachethis
=
this
;
this
.
model
.
retrieve
(
this
.
currentTab
,
function
()
{
cachethis
.
subviews
[
cachethis
.
currentTab
]
=
cachethis
.
createSubview
();
cachethis
.
subviews
[
cachethis
.
currentTab
].
render
();
});
}
else
{
// Advanced (at least) model gets created at bootstrap but the view does not
if
(
!
this
.
subviews
[
this
.
currentTab
])
{
this
.
subviews
[
this
.
currentTab
]
=
this
.
createSubview
();
}
this
.
subviews
[
this
.
currentTab
].
render
();
}
var
dateIntrospect
=
new
Date
();
this
.
$el
.
find
(
'#timezone'
).
html
(
"("
+
dateIntrospect
.
getTimezone
()
+
")"
);
return
this
;
},
createSubview
:
function
()
{
switch
(
this
.
currentTab
)
{
case
'details'
:
return
new
CMS
.
Views
.
Settings
.
Details
({
el
:
this
.
$el
.
find
(
'.settings-'
+
this
.
currentTab
),
model
:
this
.
model
.
get
(
this
.
currentTab
)
});
break
;
case
'faculty'
:
break
;
case
'grading'
:
return
new
CMS
.
Views
.
Settings
.
Grading
({
el
:
this
.
$el
.
find
(
'.settings-'
+
this
.
currentTab
),
model
:
this
.
model
.
get
(
this
.
currentTab
)
});
break
;
case
'advanced'
:
return
new
CMS
.
Views
.
Settings
.
Advanced
({
el
:
this
.
$el
.
find
(
'.settings-'
+
this
.
currentTab
),
model
:
this
.
model
.
get
(
this
.
currentTab
)
});
break
;
case
'problems'
:
break
;
case
'discussions'
:
break
;
}
},
updateTime
:
function
(
e
)
{
var
now
=
new
Date
();
var
hours
=
now
.
getHours
();
var
minutes
=
now
.
getMinutes
();
$
(
e
.
currentTarget
).
attr
(
'title'
,
(
hours
%
12
===
0
?
12
:
hours
%
12
)
+
":"
+
(
minutes
<
10
?
"0"
:
""
)
+
now
.
getMinutes
()
+
(
hours
<
12
?
"am"
:
"pm"
)
+
" (current local time)"
);
},
showSettingsTab
:
function
(
e
)
{
this
.
currentTab
=
$
(
e
.
target
).
data
(
'section'
);
$
(
'.settings-page-section > section'
).
hide
();
$
(
'.settings-'
+
this
.
currentTab
).
show
();
$
(
'.settings-page-menu .is-shown'
).
removeClass
(
'is-shown'
);
$
(
e
.
target
).
addClass
(
'is-shown'
);
// fetch model for the tab if not loaded already
this
.
render
();
}
});
CMS
.
Views
.
Settings
.
Details
=
CMS
.
Views
.
ValidatingView
.
extend
({
// Model class is CMS.Models.Settings.CourseDetails
events
:
{
"blur
input"
:
"updateModel"
,
"blur
textarea"
:
"updateModel"
,
'click .remove-course-syllabus'
:
"removeSyllabus"
,
'click .new-course-syllabus'
:
'assetSyllabus'
,
'click .remove-course-introduction-video'
:
"removeVideo"
,
'focus #course-overview'
:
"codeMirrorize"
},
initialize
:
function
()
{
// TODO move the html frag to a loaded asset
this
.
fileAnchorTemplate
=
_
.
template
(
'<a href="<%= fullpath %>"> <i class="ss-icon ss-standard">📄</i><%= filename %></a>'
);
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
},
render
:
function
()
{
this
.
setupDatePicker
(
'start_date'
);
this
.
setupDatePicker
(
'end_date'
);
this
.
setupDatePicker
(
'enrollment_start'
);
this
.
setupDatePicker
(
'enrollment_end'
);
if
(
this
.
model
.
has
(
'syllabus'
))
{
this
.
$el
.
find
(
this
.
fieldToSelectorMap
[
'syllabus'
]).
html
(
this
.
fileAnchorTemplate
({
fullpath
:
this
.
model
.
get
(
'syllabus'
),
filename
:
'syllabus'
}));
this
.
$el
.
find
(
'.remove-course-syllabus'
).
show
();
}
else
{
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'syllabus'
]).
html
(
""
);
this
.
$el
.
find
(
'.remove-course-syllabus'
).
hide
();
}
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'overview'
]).
val
(
this
.
model
.
get
(
'overview'
));
this
.
codeMirrorize
(
null
,
$
(
'#course-overview'
)[
0
]);
this
.
$el
.
find
(
'.current-course-introduction-video iframe'
).
attr
(
'src'
,
this
.
model
.
videosourceSample
());
if
(
this
.
model
.
has
(
'intro_video'
))
{
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
show
();
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'intro_video'
]).
val
(
this
.
model
.
get
(
'intro_video'
));
}
else
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'effort'
]).
val
(
this
.
model
.
get
(
'effort'
));
return
this
;
},
fieldToSelectorMap
:
{
'start_date'
:
"course-start"
,
'end_date'
:
'course-end'
,
'enrollment_start'
:
'enrollment-start'
,
'enrollment_end'
:
'enrollment-end'
,
'syllabus'
:
'.current-course-syllabus .doc-filename'
,
'overview'
:
'course-overview'
,
'intro_video'
:
'course-introduction-video'
,
'effort'
:
"course-effort"
},
// Model class is CMS.Models.Settings.CourseDetails
events
:
{
"change
input"
:
"updateModel"
,
"change
textarea"
:
"updateModel"
,
'click .remove-course-syllabus'
:
"removeSyllabus"
,
'click .new-course-syllabus'
:
'assetSyllabus'
,
'click .remove-course-introduction-video'
:
"removeVideo"
,
'focus #course-overview'
:
"codeMirrorize"
},
initialize
:
function
()
{
// TODO move the html frag to a loaded asset
this
.
fileAnchorTemplate
=
_
.
template
(
'<a href="<%= fullpath %>"> <i class="ss-icon ss-standard">📄</i><%= filename %></a>'
);
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
},
render
:
function
()
{
this
.
setupDatePicker
(
'start_date'
);
this
.
setupDatePicker
(
'end_date'
);
this
.
setupDatePicker
(
'enrollment_start'
);
this
.
setupDatePicker
(
'enrollment_end'
);
if
(
this
.
model
.
has
(
'syllabus'
))
{
this
.
$el
.
find
(
this
.
fieldToSelectorMap
[
'syllabus'
]).
html
(
this
.
fileAnchorTemplate
({
fullpath
:
this
.
model
.
get
(
'syllabus'
),
filename
:
'syllabus'
}));
this
.
$el
.
find
(
'.remove-course-syllabus'
).
show
();
}
else
{
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'syllabus'
]).
html
(
""
);
this
.
$el
.
find
(
'.remove-course-syllabus'
).
hide
();
}
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'overview'
]).
val
(
this
.
model
.
get
(
'overview'
));
this
.
codeMirrorize
(
null
,
$
(
'#course-overview'
)[
0
]);
this
.
$el
.
find
(
'.current-course-introduction-video iframe'
).
attr
(
'src'
,
this
.
model
.
videosourceSample
());
if
(
this
.
model
.
has
(
'intro_video'
))
{
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
show
();
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'intro_video'
]).
val
(
this
.
model
.
get
(
'intro_video'
));
}
else
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'effort'
]).
val
(
this
.
model
.
get
(
'effort'
));
return
this
;
},
fieldToSelectorMap
:
{
'start_date'
:
"course-start"
,
'end_date'
:
'course-end'
,
'enrollment_start'
:
'enrollment-start'
,
'enrollment_end'
:
'enrollment-end'
,
'syllabus'
:
'.current-course-syllabus .doc-filename'
,
'overview'
:
'course-overview'
,
'intro_video'
:
'course-introduction-video'
,
'effort'
:
"course-effort"
},
setupDatePicker
:
function
(
fieldName
)
{
var
cacheModel
=
this
.
model
;
...
...
@@ -245,58 +260,58 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
datefield
.
datepicker
(
'setDate'
,
this
.
model
.
get
(
fieldName
));
if
(
this
.
model
.
has
(
fieldName
))
timefield
.
timepicker
(
'setTime'
,
this
.
model
.
get
(
fieldName
));
},
updateModel
:
function
(
event
)
{
switch
(
event
.
currentTarget
.
id
)
{
case
'course-start-date'
:
// handled via onSelect method
case
'course-end-date'
:
case
'course-enrollment-start-date'
:
case
'course-enrollment-end-date'
:
break
;
case
'course-overview'
:
// handled via code mirror
break
;
case
'course-effort'
:
this
.
saveIfChanged
(
event
);
break
;
case
'course-introduction-video'
:
this
.
clearValidationErrors
();
var
previewsource
=
this
.
model
.
save_videosource
(
$
(
event
.
currentTarget
).
val
());
this
.
$el
.
find
(
".current-course-introduction-video iframe"
).
attr
(
"src"
,
previewsource
);
if
(
this
.
model
.
has
(
'intro_video'
))
{
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
show
();
}
else
{
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
}
break
;
default
:
break
;
}
},
removeSyllabus
:
function
()
{
if
(
this
.
model
.
has
(
'syllabus'
))
this
.
model
.
save
({
'syllabus'
:
null
},
{
error
:
CMS
.
ServerError
});
},
assetSyllabus
:
function
()
{
// TODO implement
},
removeVideo
:
function
()
{
if
(
this
.
model
.
has
(
'intro_video'
))
{
this
.
model
.
save_videosource
(
null
);
this
.
$el
.
find
(
".current-course-introduction-video iframe"
).
attr
(
"src"
,
""
);
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'intro_video'
]).
val
(
""
);
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
}
},
codeMirrors
:
{},
updateModel
:
function
(
event
)
{
switch
(
event
.
currentTarget
.
id
)
{
case
'course-start-date'
:
// handled via onSelect method
case
'course-end-date'
:
case
'course-enrollment-start-date'
:
case
'course-enrollment-end-date'
:
break
;
case
'course-overview'
:
// handled via code mirror
break
;
case
'course-effort'
:
this
.
saveIfChanged
(
event
);
break
;
case
'course-introduction-video'
:
this
.
clearValidationErrors
();
var
previewsource
=
this
.
model
.
save_videosource
(
$
(
event
.
currentTarget
).
val
());
this
.
$el
.
find
(
".current-course-introduction-video iframe"
).
attr
(
"src"
,
previewsource
);
if
(
this
.
model
.
has
(
'intro_video'
))
{
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
show
();
}
else
{
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
}
break
;
default
:
break
;
}
},
removeSyllabus
:
function
()
{
if
(
this
.
model
.
has
(
'syllabus'
))
this
.
model
.
save
({
'syllabus'
:
null
},
{
error
:
CMS
.
ServerError
});
},
assetSyllabus
:
function
()
{
// TODO implement
},
removeVideo
:
function
()
{
if
(
this
.
model
.
has
(
'intro_video'
))
{
this
.
model
.
save_videosource
(
null
);
this
.
$el
.
find
(
".current-course-introduction-video iframe"
).
attr
(
"src"
,
""
);
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'intro_video'
]).
val
(
""
);
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
}
},
codeMirrors
:
{},
codeMirrorize
:
function
(
e
,
forcedTarget
)
{
var
thisTarget
;
if
(
forcedTarget
)
{
...
...
@@ -316,42 +331,42 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
cachethis
.
clearValidationErrors
();
var
newVal
=
mirror
.
getValue
();
if
(
cachethis
.
model
.
get
(
field
)
!=
newVal
)
cachethis
.
model
.
save
(
field
,
newVal
,
{
error
:
CMS
.
ServerError
});
{
error
:
CMS
.
ServerError
});
}
});
}
}
});
CMS
.
Views
.
Settings
.
Grading
=
CMS
.
Views
.
ValidatingView
.
extend
({
// Model class is CMS.Models.Settings.CourseGradingPolicy
events
:
{
"blur
input"
:
"updateModel"
,
"blur
textarea"
:
"updateModel"
,
"blur
span[contenteditable=true]"
:
"updateDesignation"
,
"click .settings-extra header"
:
"showSettingsExtras"
,
"click .new-grade-button"
:
"addNewGrade"
,
"click .remove-button"
:
"removeGrade"
,
"click .add-grading-data"
:
"addAssignmentType"
},
initialize
:
function
()
{
// load template for grading view
var
self
=
this
;
// Model class is CMS.Models.Settings.CourseGradingPolicy
events
:
{
"change
input"
:
"updateModel"
,
"change
textarea"
:
"updateModel"
,
"change
span[contenteditable=true]"
:
"updateDesignation"
,
"click .settings-extra header"
:
"showSettingsExtras"
,
"click .new-grade-button"
:
"addNewGrade"
,
"click .remove-button"
:
"removeGrade"
,
"click .add-grading-data"
:
"addAssignmentType"
},
initialize
:
function
()
{
// load template for grading view
var
self
=
this
;
this
.
gradeCutoffTemplate
=
_
.
template
(
'<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable>'
+
'<%= descriptor %>'
+
'</span><span class="range"></span>'
+
'<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>'
+
'</li>'
);
'<%= descriptor %>'
+
'</span><span class="range"></span>'
+
'<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>'
+
'</li>'
);
// Instrument grading scale
// convert cutoffs to inversely ordered list
var
modelCutoffs
=
this
.
model
.
get
(
'grade_cutoffs'
);
for
(
var
cutoff
in
modelCutoffs
)
{
this
.
descendingCutoffs
.
push
({
designation
:
cutoff
,
cutoff
:
Math
.
round
(
modelCutoffs
[
cutoff
]
*
100
)});
this
.
descendingCutoffs
.
push
({
designation
:
cutoff
,
cutoff
:
Math
.
round
(
modelCutoffs
[
cutoff
]
*
100
)});
}
this
.
descendingCutoffs
=
_
.
sortBy
(
this
.
descendingCutoffs
,
function
(
gradeEle
)
{
return
-
gradeEle
[
'cutoff'
];
});
function
(
gradeEle
)
{
return
-
gradeEle
[
'cutoff'
];
});
// Instrument grace period
this
.
$el
.
find
(
'#course-grading-graceperiod'
).
timepicker
();
...
...
@@ -359,330 +374,330 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
// instantiates an editor template for each update in the collection
// Because this calls render, put it after everything which render may depend upon to prevent race condition.
window
.
templateLoader
.
loadRemoteTemplate
(
"course_grade_policy"
,
"/static/client_templates/course_grade_policy.html"
,
function
(
raw_template
)
{
self
.
template
=
_
.
template
(
raw_template
);
self
.
render
();
"/static/client_templates/course_grade_policy.html"
,
function
(
raw_template
)
{
self
.
template
=
_
.
template
(
raw_template
);
self
.
render
();
}
);
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'remove'
,
this
.
render
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'reset'
,
this
.
render
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'add'
,
this
.
render
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
},
render
:
function
()
{
// prevent bootstrap race condition by event dispatch
if
(
!
this
.
template
)
return
;
// Create and render the grading type subs
var
self
=
this
;
var
gradelist
=
this
.
$el
.
find
(
'.course-grading-assignment-list'
);
// Undo the double invocation error. At some point, fix the double invocation
$
(
gradelist
).
empty
();
var
gradeCollection
=
this
.
model
.
get
(
'graders'
);
gradeCollection
.
each
(
function
(
gradeModel
)
{
$
(
gradelist
).
append
(
self
.
template
({
model
:
gradeModel
}));
var
newEle
=
gradelist
.
children
().
last
();
var
newView
=
new
CMS
.
Views
.
Settings
.
GraderView
({
el
:
newEle
,
model
:
gradeModel
,
collection
:
gradeCollection
});
});
// render the grade cutoffs
this
.
renderCutoffBar
();
var
graceEle
=
this
.
$el
.
find
(
'#course-grading-graceperiod'
);
graceEle
.
timepicker
({
'timeFormat'
:
'H:i'
});
// init doesn't take setTime
if
(
this
.
model
.
has
(
'grace_period'
))
graceEle
.
timepicker
(
'setTime'
,
this
.
model
.
gracePeriodToDate
());
// remove any existing listeners to keep them from piling on b/c render gets called frequently
graceEle
.
off
(
'change'
,
this
.
setGracePeriod
);
graceEle
.
on
(
'change'
,
this
,
this
.
setGracePeriod
);
return
this
;
},
addAssignmentType
:
function
(
e
)
{
e
.
preventDefault
();
this
.
model
.
get
(
'graders'
).
push
({});
},
fieldToSelectorMap
:
{
'grace_period'
:
'course-grading-graceperiod'
},
setGracePeriod
:
function
(
event
)
{
event
.
data
.
clearValidationErrors
();
var
newVal
=
event
.
data
.
model
.
dateToGracePeriod
(
$
(
event
.
currentTarget
).
timepicker
(
'getTime'
));
if
(
event
.
data
.
model
.
get
(
'grace_period'
)
!=
newVal
)
event
.
data
.
model
.
save
(
'grace_period'
,
newVal
,
{
error
:
CMS
.
ServerError
});
},
updateModel
:
function
(
event
)
{
if
(
!
this
.
selectorToField
[
event
.
currentTarget
.
id
])
return
;
switch
(
this
.
selectorToField
[
event
.
currentTarget
.
id
])
{
case
'grace_period'
:
// handled above
break
;
default
:
this
.
saveIfChanged
(
event
);
break
;
}
},
// Grade sliders attributes and methods
// Grade bars are li's ordered A -> F with A taking whole width, B overlaying it with its paint, ...
// The actual cutoff for each grade is the width % of the next lower grade; so, the hack here
// is to lay down a whole width bar claiming it's A and then lay down bars for each actual grade
// starting w/ A but posting the label in the preceding li and setting the label of the last to "Fail" or "F"
// A does not have a drag bar (cannot change its upper limit)
// Need to insert new bars in right place.
GRADES
:
[
'A'
,
'B'
,
'C'
,
'D'
],
// defaults for new grade designators
descendingCutoffs
:
[],
// array of { designation : , cutoff : }
gradeBarWidth
:
null
,
// cache of value since it won't change (more certain)
renderCutoffBar
:
function
()
{
var
gradeBar
=
this
.
$el
.
find
(
'.grade-bar'
);
this
.
gradeBarWidth
=
gradeBar
.
width
();
var
gradelist
=
gradeBar
.
children
(
'.grades'
);
// HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
gradelist
.
empty
();
var
nextWidth
=
100
;
// first width is 100%
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'remove'
,
this
.
render
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'reset'
,
this
.
render
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'add'
,
this
.
render
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
},
render
:
function
()
{
// prevent bootstrap race condition by event dispatch
if
(
!
this
.
template
)
return
;
// Create and render the grading type subs
var
self
=
this
;
var
gradelist
=
this
.
$el
.
find
(
'.course-grading-assignment-list'
);
// Undo the double invocation error. At some point, fix the double invocation
$
(
gradelist
).
empty
();
var
gradeCollection
=
this
.
model
.
get
(
'graders'
);
gradeCollection
.
each
(
function
(
gradeModel
)
{
$
(
gradelist
).
append
(
self
.
template
({
model
:
gradeModel
}));
var
newEle
=
gradelist
.
children
().
last
();
var
newView
=
new
CMS
.
Views
.
Settings
.
GraderView
({
el
:
newEle
,
model
:
gradeModel
,
collection
:
gradeCollection
});
});
// render the grade cutoffs
this
.
renderCutoffBar
();
var
graceEle
=
this
.
$el
.
find
(
'#course-grading-graceperiod'
);
graceEle
.
timepicker
({
'timeFormat'
:
'H:i'
});
// init doesn't take setTime
if
(
this
.
model
.
has
(
'grace_period'
))
graceEle
.
timepicker
(
'setTime'
,
this
.
model
.
gracePeriodToDate
());
// remove any existing listeners to keep them from piling on b/c render gets called frequently
graceEle
.
off
(
'change'
,
this
.
setGracePeriod
);
graceEle
.
on
(
'change'
,
this
,
this
.
setGracePeriod
);
return
this
;
},
addAssignmentType
:
function
(
e
)
{
e
.
preventDefault
();
this
.
model
.
get
(
'graders'
).
push
({});
},
fieldToSelectorMap
:
{
'grace_period'
:
'course-grading-graceperiod'
},
setGracePeriod
:
function
(
event
)
{
event
.
data
.
clearValidationErrors
();
var
newVal
=
event
.
data
.
model
.
dateToGracePeriod
(
$
(
event
.
currentTarget
).
timepicker
(
'getTime'
));
if
(
event
.
data
.
model
.
get
(
'grace_period'
)
!=
newVal
)
event
.
data
.
model
.
save
(
'grace_period'
,
newVal
,
{
error
:
CMS
.
ServerError
});
},
updateModel
:
function
(
event
)
{
if
(
!
this
.
selectorToField
[
event
.
currentTarget
.
id
])
return
;
switch
(
this
.
selectorToField
[
event
.
currentTarget
.
id
])
{
case
'grace_period'
:
// handled above
break
;
default
:
this
.
saveIfChanged
(
event
);
break
;
}
},
// Grade sliders attributes and methods
// Grade bars are li's ordered A -> F with A taking whole width, B overlaying it with its paint, ...
// The actual cutoff for each grade is the width % of the next lower grade; so, the hack here
// is to lay down a whole width bar claiming it's A and then lay down bars for each actual grade
// starting w/ A but posting the label in the preceding li and setting the label of the last to "Fail" or "F"
// A does not have a drag bar (cannot change its upper limit)
// Need to insert new bars in right place.
GRADES
:
[
'A'
,
'B'
,
'C'
,
'D'
],
// defaults for new grade designators
descendingCutoffs
:
[],
// array of { designation : , cutoff : }
gradeBarWidth
:
null
,
// cache of value since it won't change (more certain)
renderCutoffBar
:
function
()
{
var
gradeBar
=
this
.
$el
.
find
(
'.grade-bar'
);
this
.
gradeBarWidth
=
gradeBar
.
width
();
var
gradelist
=
gradeBar
.
children
(
'.grades'
);
// HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
gradelist
.
empty
();
var
nextWidth
=
100
;
// first width is 100%
// Can probably be simplified to one variable now.
var
removable
=
false
;
var
draggable
=
false
;
// first and last are not removable, first is not draggable
_
.
each
(
this
.
descendingCutoffs
,
function
(
cutoff
,
index
)
{
var
newBar
=
this
.
gradeCutoffTemplate
({
descriptor
:
cutoff
[
'designation'
]
,
width
:
nextWidth
,
removable
:
removable
});
gradelist
.
append
(
newBar
);
if
(
draggable
)
{
newBar
=
gradelist
.
children
().
last
();
// get the dom object not the unparsed string
newBar
.
resizable
({
handles
:
"e"
,
containment
:
"parent"
,
start
:
this
.
startMoveClosure
(),
resize
:
this
.
moveBarClosure
(),
stop
:
this
.
stopDragClosure
()
});
}
// prepare for next
nextWidth
=
cutoff
[
'cutoff'
];
removable
=
true
;
// first is not removable, all others are
draggable
=
true
;
},
this
);
// add fail which is not in data
var
failBar
=
this
.
gradeCutoffTemplate
({
descriptor
:
this
.
failLabel
(),
width
:
nextWidth
,
removable
:
false
});
$
(
failBar
).
find
(
"span[contenteditable=true]"
).
attr
(
"contenteditable"
,
false
);
gradelist
.
append
(
failBar
);
gradelist
.
children
().
last
().
resizable
({
handles
:
"e"
,
containment
:
"parent"
,
start
:
this
.
startMoveClosure
(),
resize
:
this
.
moveBarClosure
(),
stop
:
this
.
stopDragClosure
()
});
this
.
renderGradeRanges
();
},
showSettingsExtras
:
function
(
event
)
{
$
(
event
.
currentTarget
).
toggleClass
(
'active'
);
$
(
event
.
currentTarget
).
siblings
.
toggleClass
(
'is-shown'
);
},
startMoveClosure
:
function
()
{
// set min/max widths
var
cachethis
=
this
;
var
widthPerPoint
=
cachethis
.
gradeBarWidth
/
100
;
return
function
(
event
,
ui
)
{
var
barIndex
=
ui
.
element
.
index
();
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
var
min
=
(
barIndex
<
cachethis
.
descendingCutoffs
.
length
?
cachethis
.
descendingCutoffs
[
barIndex
][
'cutoff'
]
+
3
:
3
);
// minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
var
max
=
(
barIndex
>=
2
?
cachethis
.
descendingCutoffs
[
barIndex
-
2
][
'cutoff'
]
-
3
:
97
);
ui
.
element
.
resizable
(
"option"
,{
minWidth
:
min
*
widthPerPoint
,
maxWidth
:
max
*
widthPerPoint
});
};
},
moveBarClosure
:
function
()
{
// 0th ele doesn't have a bar; so, will never invoke this
var
cachethis
=
this
;
return
function
(
event
,
ui
)
{
var
barIndex
=
ui
.
element
.
index
();
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
var
min
=
(
barIndex
<
cachethis
.
descendingCutoffs
.
length
?
cachethis
.
descendingCutoffs
[
barIndex
][
'cutoff'
]
+
3
:
3
);
// minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
var
max
=
(
barIndex
>=
2
?
cachethis
.
descendingCutoffs
[
barIndex
-
2
][
'cutoff'
]
-
3
:
100
);
var
percentage
=
Math
.
min
(
Math
.
max
(
ui
.
size
.
width
/
cachethis
.
gradeBarWidth
*
100
,
min
),
max
);
cachethis
.
descendingCutoffs
[
barIndex
-
1
][
'cutoff'
]
=
Math
.
round
(
percentage
);
cachethis
.
renderGradeRanges
();
};
},
renderGradeRanges
:
function
()
{
// the labels showing the range e.g., 71-80
var
cutoffs
=
this
.
descendingCutoffs
;
this
.
$el
.
find
(
'.range'
).
each
(
function
(
i
)
{
var
min
=
(
i
<
cutoffs
.
length
?
cutoffs
[
i
][
'cutoff'
]
:
0
);
var
max
=
(
i
>
0
?
cutoffs
[
i
-
1
][
'cutoff'
]
:
100
);
$
(
this
).
text
(
min
+
'-'
+
max
);
});
},
stopDragClosure
:
function
()
{
var
cachethis
=
this
;
return
function
(
event
,
ui
)
{
// for some reason the resize is setting height to 0
cachethis
.
saveCutoffs
();
};
},
saveCutoffs
:
function
()
{
this
.
model
.
save
(
'grade_cutoffs'
,
_
.
reduce
(
this
.
descendingCutoffs
,
function
(
object
,
cutoff
)
{
object
[
cutoff
[
'designation'
]]
=
cutoff
[
'cutoff'
]
/
100.0
;
return
object
;
},
{}),
{
error
:
CMS
.
ServerError
});
},
addNewGrade
:
function
(
e
)
{
e
.
preventDefault
();
var
gradeLength
=
this
.
descendingCutoffs
.
length
;
// cutoffs doesn't include fail/f so this is only the passing grades
if
(
gradeLength
>
3
)
{
// TODO shouldn't we disable the button
return
;
}
var
failBarWidth
=
this
.
descendingCutoffs
[
gradeLength
-
1
][
'cutoff'
];
// going to split the grade above the insertion point in half leaving fail in same place
var
nextGradeTop
=
(
gradeLength
>
1
?
this
.
descendingCutoffs
[
gradeLength
-
2
][
'cutoff'
]
:
100
);
var
targetWidth
=
failBarWidth
+
((
nextGradeTop
-
failBarWidth
)
/
2
);
this
.
descendingCutoffs
.
push
({
designation
:
this
.
GRADES
[
gradeLength
],
cutoff
:
failBarWidth
});
this
.
descendingCutoffs
[
gradeLength
-
1
][
'cutoff'
]
=
Math
.
round
(
targetWidth
);
var
$newGradeBar
=
this
.
gradeCutoffTemplate
({
descriptor
:
this
.
GRADES
[
gradeLength
],
width
:
targetWidth
,
removable
:
true
});
var
gradeDom
=
this
.
$el
.
find
(
'.grades'
);
gradeDom
.
children
().
last
().
before
(
$newGradeBar
);
var
newEle
=
gradeDom
.
children
()[
gradeLength
];
$
(
newEle
).
resizable
({
handles
:
"e"
,
containment
:
"parent"
,
start
:
this
.
startMoveClosure
(),
resize
:
this
.
moveBarClosure
(),
stop
:
this
.
stopDragClosure
()
});
// Munge existing grade labels?
// If going from Pass/Fail to 3 levels, change to Pass to A
if
(
gradeLength
===
1
&&
this
.
descendingCutoffs
[
0
][
'designation'
]
===
'Pass'
)
{
this
.
descendingCutoffs
[
0
][
'designation'
]
=
this
.
GRADES
[
0
];
this
.
setTopGradeLabel
();
}
this
.
setFailLabel
();
this
.
renderGradeRanges
();
this
.
saveCutoffs
();
},
removeGrade
:
function
(
e
)
{
e
.
preventDefault
();
var
domElement
=
$
(
e
.
currentTarget
).
closest
(
'li'
);
var
index
=
domElement
.
index
();
// copy the boundary up to the next higher grade then remove
this
.
descendingCutoffs
[
index
-
1
][
'cutoff'
]
=
this
.
descendingCutoffs
[
index
][
'cutoff'
];
this
.
descendingCutoffs
.
splice
(
index
,
1
);
domElement
.
remove
();
if
(
this
.
descendingCutoffs
.
length
===
1
&&
this
.
descendingCutoffs
[
0
][
'designation'
]
===
this
.
GRADES
[
0
])
{
this
.
descendingCutoffs
[
0
][
'designation'
]
=
'Pass'
;
this
.
setTopGradeLabel
();
}
this
.
setFailLabel
();
this
.
renderGradeRanges
();
this
.
saveCutoffs
();
},
updateDesignation
:
function
(
e
)
{
var
index
=
$
(
e
.
currentTarget
).
closest
(
'li'
).
index
();
this
.
descendingCutoffs
[
index
][
'designation'
]
=
$
(
e
.
currentTarget
).
html
();
this
.
saveCutoffs
();
},
failLabel
:
function
()
{
if
(
this
.
descendingCutoffs
.
length
===
1
)
return
'Fail'
;
else
return
'F'
;
},
setFailLabel
:
function
()
{
this
.
$el
.
find
(
'.grades .letter-grade'
).
last
().
html
(
this
.
failLabel
());
},
setTopGradeLabel
:
function
()
{
this
.
$el
.
find
(
'.grades .letter-grade'
).
first
().
html
(
this
.
descendingCutoffs
[
0
][
'designation'
]);
}
_
.
each
(
this
.
descendingCutoffs
,
function
(
cutoff
,
index
)
{
var
newBar
=
this
.
gradeCutoffTemplate
({
descriptor
:
cutoff
[
'designation'
]
,
width
:
nextWidth
,
removable
:
removable
});
gradelist
.
append
(
newBar
);
if
(
draggable
)
{
newBar
=
gradelist
.
children
().
last
();
// get the dom object not the unparsed string
newBar
.
resizable
({
handles
:
"e"
,
containment
:
"parent"
,
start
:
this
.
startMoveClosure
(),
resize
:
this
.
moveBarClosure
(),
stop
:
this
.
stopDragClosure
()
});
}
// prepare for next
nextWidth
=
cutoff
[
'cutoff'
];
removable
=
true
;
// first is not removable, all others are
draggable
=
true
;
},
this
);
// add fail which is not in data
var
failBar
=
this
.
gradeCutoffTemplate
({
descriptor
:
this
.
failLabel
(),
width
:
nextWidth
,
removable
:
false
});
$
(
failBar
).
find
(
"span[contenteditable=true]"
).
attr
(
"contenteditable"
,
false
);
gradelist
.
append
(
failBar
);
gradelist
.
children
().
last
().
resizable
({
handles
:
"e"
,
containment
:
"parent"
,
start
:
this
.
startMoveClosure
(),
resize
:
this
.
moveBarClosure
(),
stop
:
this
.
stopDragClosure
()
});
this
.
renderGradeRanges
();
},
showSettingsExtras
:
function
(
event
)
{
$
(
event
.
currentTarget
).
toggleClass
(
'active'
);
$
(
event
.
currentTarget
).
siblings
.
toggleClass
(
'is-shown'
);
},
startMoveClosure
:
function
()
{
// set min/max widths
var
cachethis
=
this
;
var
widthPerPoint
=
cachethis
.
gradeBarWidth
/
100
;
return
function
(
event
,
ui
)
{
var
barIndex
=
ui
.
element
.
index
();
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
var
min
=
(
barIndex
<
cachethis
.
descendingCutoffs
.
length
?
cachethis
.
descendingCutoffs
[
barIndex
][
'cutoff'
]
+
3
:
3
);
// minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
var
max
=
(
barIndex
>=
2
?
cachethis
.
descendingCutoffs
[
barIndex
-
2
][
'cutoff'
]
-
3
:
97
);
ui
.
element
.
resizable
(
"option"
,{
minWidth
:
min
*
widthPerPoint
,
maxWidth
:
max
*
widthPerPoint
});
};
},
moveBarClosure
:
function
()
{
// 0th ele doesn't have a bar; so, will never invoke this
var
cachethis
=
this
;
return
function
(
event
,
ui
)
{
var
barIndex
=
ui
.
element
.
index
();
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
var
min
=
(
barIndex
<
cachethis
.
descendingCutoffs
.
length
?
cachethis
.
descendingCutoffs
[
barIndex
][
'cutoff'
]
+
3
:
3
);
// minus 2 b/c minus 1 is the element we're effecting. It's max is just shy of the next one above it
var
max
=
(
barIndex
>=
2
?
cachethis
.
descendingCutoffs
[
barIndex
-
2
][
'cutoff'
]
-
3
:
100
);
var
percentage
=
Math
.
min
(
Math
.
max
(
ui
.
size
.
width
/
cachethis
.
gradeBarWidth
*
100
,
min
),
max
);
cachethis
.
descendingCutoffs
[
barIndex
-
1
][
'cutoff'
]
=
Math
.
round
(
percentage
);
cachethis
.
renderGradeRanges
();
};
},
renderGradeRanges
:
function
()
{
// the labels showing the range e.g., 71-80
var
cutoffs
=
this
.
descendingCutoffs
;
this
.
$el
.
find
(
'.range'
).
each
(
function
(
i
)
{
var
min
=
(
i
<
cutoffs
.
length
?
cutoffs
[
i
][
'cutoff'
]
:
0
);
var
max
=
(
i
>
0
?
cutoffs
[
i
-
1
][
'cutoff'
]
:
100
);
$
(
this
).
text
(
min
+
'-'
+
max
);
});
},
stopDragClosure
:
function
()
{
var
cachethis
=
this
;
return
function
(
event
,
ui
)
{
// for some reason the resize is setting height to 0
cachethis
.
saveCutoffs
();
};
},
saveCutoffs
:
function
()
{
this
.
model
.
save
(
'grade_cutoffs'
,
_
.
reduce
(
this
.
descendingCutoffs
,
function
(
object
,
cutoff
)
{
object
[
cutoff
[
'designation'
]]
=
cutoff
[
'cutoff'
]
/
100.0
;
return
object
;
},
{}),
{
error
:
CMS
.
ServerError
});
},
addNewGrade
:
function
(
e
)
{
e
.
preventDefault
();
var
gradeLength
=
this
.
descendingCutoffs
.
length
;
// cutoffs doesn't include fail/f so this is only the passing grades
if
(
gradeLength
>
3
)
{
// TODO shouldn't we disable the button
return
;
}
var
failBarWidth
=
this
.
descendingCutoffs
[
gradeLength
-
1
][
'cutoff'
];
// going to split the grade above the insertion point in half leaving fail in same place
var
nextGradeTop
=
(
gradeLength
>
1
?
this
.
descendingCutoffs
[
gradeLength
-
2
][
'cutoff'
]
:
100
);
var
targetWidth
=
failBarWidth
+
((
nextGradeTop
-
failBarWidth
)
/
2
);
this
.
descendingCutoffs
.
push
({
designation
:
this
.
GRADES
[
gradeLength
],
cutoff
:
failBarWidth
});
this
.
descendingCutoffs
[
gradeLength
-
1
][
'cutoff'
]
=
Math
.
round
(
targetWidth
);
var
$newGradeBar
=
this
.
gradeCutoffTemplate
({
descriptor
:
this
.
GRADES
[
gradeLength
],
width
:
targetWidth
,
removable
:
true
});
var
gradeDom
=
this
.
$el
.
find
(
'.grades'
);
gradeDom
.
children
().
last
().
before
(
$newGradeBar
);
var
newEle
=
gradeDom
.
children
()[
gradeLength
];
$
(
newEle
).
resizable
({
handles
:
"e"
,
containment
:
"parent"
,
start
:
this
.
startMoveClosure
(),
resize
:
this
.
moveBarClosure
(),
stop
:
this
.
stopDragClosure
()
});
// Munge existing grade labels?
// If going from Pass/Fail to 3 levels, change to Pass to A
if
(
gradeLength
===
1
&&
this
.
descendingCutoffs
[
0
][
'designation'
]
===
'Pass'
)
{
this
.
descendingCutoffs
[
0
][
'designation'
]
=
this
.
GRADES
[
0
];
this
.
setTopGradeLabel
();
}
this
.
setFailLabel
();
this
.
renderGradeRanges
();
this
.
saveCutoffs
();
},
removeGrade
:
function
(
e
)
{
e
.
preventDefault
();
var
domElement
=
$
(
e
.
currentTarget
).
closest
(
'li'
);
var
index
=
domElement
.
index
();
// copy the boundary up to the next higher grade then remove
this
.
descendingCutoffs
[
index
-
1
][
'cutoff'
]
=
this
.
descendingCutoffs
[
index
][
'cutoff'
];
this
.
descendingCutoffs
.
splice
(
index
,
1
);
domElement
.
remove
();
if
(
this
.
descendingCutoffs
.
length
===
1
&&
this
.
descendingCutoffs
[
0
][
'designation'
]
===
this
.
GRADES
[
0
])
{
this
.
descendingCutoffs
[
0
][
'designation'
]
=
'Pass'
;
this
.
setTopGradeLabel
();
}
this
.
setFailLabel
();
this
.
renderGradeRanges
();
this
.
saveCutoffs
();
},
updateDesignation
:
function
(
e
)
{
var
index
=
$
(
e
.
currentTarget
).
closest
(
'li'
).
index
();
this
.
descendingCutoffs
[
index
][
'designation'
]
=
$
(
e
.
currentTarget
).
html
();
this
.
saveCutoffs
();
},
failLabel
:
function
()
{
if
(
this
.
descendingCutoffs
.
length
===
1
)
return
'Fail'
;
else
return
'F'
;
},
setFailLabel
:
function
()
{
this
.
$el
.
find
(
'.grades .letter-grade'
).
last
().
html
(
this
.
failLabel
());
},
setTopGradeLabel
:
function
()
{
this
.
$el
.
find
(
'.grades .letter-grade'
).
first
().
html
(
this
.
descendingCutoffs
[
0
][
'designation'
]);
}
});
CMS
.
Views
.
Settings
.
GraderView
=
CMS
.
Views
.
ValidatingView
.
extend
({
// Model class is CMS.Models.Settings.CourseGrader
events
:
{
"blur
input"
:
"updateModel"
,
"blur
textarea"
:
"updateModel"
,
"click .remove-grading-data"
:
"deleteModel"
},
initialize
:
function
()
{
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
this
.
render
();
},
render
:
function
()
{
return
this
;
},
fieldToSelectorMap
:
{
'type'
:
'course-grading-assignment-name'
,
'short_label'
:
'course-grading-assignment-shortname'
,
'min_count'
:
'course-grading-assignment-totalassignments'
,
'drop_count'
:
'course-grading-assignment-droppable'
,
'weight'
:
'course-grading-assignment-gradeweight'
},
updateModel
:
function
(
event
)
{
// HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving
// this in out of paranoia. If this error ever happens, the user will get a warning that they cannot
// give 2 assignments the same name.]
if
(
!
this
.
model
.
collection
)
{
this
.
model
.
collection
=
this
.
collection
;
}
switch
(
event
.
currentTarget
.
id
)
{
case
'course-grading-assignment-totalassignments'
:
this
.
$el
.
find
(
'#course-grading-assignment-droppable'
).
attr
(
'max'
,
$
(
event
.
currentTarget
).
val
());
this
.
saveIfChanged
(
event
);
break
;
case
'course-grading-assignment-name'
:
var
oldName
=
this
.
model
.
get
(
'type'
);
if
(
this
.
saveIfChanged
(
event
)
&&
!
_
.
isEmpty
(
oldName
))
{
// overload the error display logic
this
.
_cacheValidationErrors
.
push
(
event
.
currentTarget
);
$
(
event
.
currentTarget
).
parent
().
append
(
this
.
errorTemplate
({
message
:
'For grading to work, you must change all "'
+
oldName
+
'" subsections to "'
+
this
.
model
.
get
(
'type'
)
+
'".'
}));
}
break
;
default
:
this
.
saveIfChanged
(
event
);
break
;
}
},
deleteModel
:
function
(
e
)
{
this
.
model
.
destroy
(
{
error
:
CMS
.
ServerError
});
e
.
preventDefault
();
}
// Model class is CMS.Models.Settings.CourseGrader
events
:
{
"change
input"
:
"updateModel"
,
"change
textarea"
:
"updateModel"
,
"click .remove-grading-data"
:
"deleteModel"
},
initialize
:
function
()
{
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
this
.
render
();
},
render
:
function
()
{
return
this
;
},
fieldToSelectorMap
:
{
'type'
:
'course-grading-assignment-name'
,
'short_label'
:
'course-grading-assignment-shortname'
,
'min_count'
:
'course-grading-assignment-totalassignments'
,
'drop_count'
:
'course-grading-assignment-droppable'
,
'weight'
:
'course-grading-assignment-gradeweight'
},
updateModel
:
function
(
event
)
{
// HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving
// this in out of paranoia. If this error ever happens, the user will get a warning that they cannot
// give 2 assignments the same name.]
if
(
!
this
.
model
.
collection
)
{
this
.
model
.
collection
=
this
.
collection
;
}
switch
(
event
.
currentTarget
.
id
)
{
case
'course-grading-assignment-totalassignments'
:
this
.
$el
.
find
(
'#course-grading-assignment-droppable'
).
attr
(
'max'
,
$
(
event
.
currentTarget
).
val
());
this
.
saveIfChanged
(
event
);
break
;
case
'course-grading-assignment-name'
:
var
oldName
=
this
.
model
.
get
(
'type'
);
if
(
this
.
saveIfChanged
(
event
)
&&
!
_
.
isEmpty
(
oldName
))
{
// overload the error display logic
this
.
_cacheValidationErrors
.
push
(
event
.
currentTarget
);
$
(
event
.
currentTarget
).
parent
().
append
(
this
.
errorTemplate
({
message
:
'For grading to work, you must change all "'
+
oldName
+
'" subsections to "'
+
this
.
model
.
get
(
'type'
)
+
'".'
}));
}
break
;
default
:
this
.
saveIfChanged
(
event
);
break
;
}
},
deleteModel
:
function
(
e
)
{
this
.
model
.
destroy
(
{
error
:
CMS
.
ServerError
});
e
.
preventDefault
();
}
});
\ No newline at end of file
cms/templates/settings.html
View file @
8f16d639
<
%
inherit
file=
"base.html"
/>
<
%!
from
django
.
core
.
urlresolvers
import
reverse
%
>
<
%
block
name=
"bodyclass"
>
settings
</
%
block>
<
%
block
name=
"title"
>
Settings
</
%
block>
...
...
@@ -15,24 +16,28 @@ from contentstore import utils
<script
src=
"${static.url('js/vendor/date.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/template_loader.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/views/server_error.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/models/course_relative.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/models/settings/advanced.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/models/settings/course_details.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/models/settings/course_settings.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/models/settings/course_grading_policy.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/views/settings/main_settings_view.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/
views/server_error
.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/
models/settings/advanced
.js')}"
></script>
<script
type=
"text/javascript"
>
$
(
document
).
ready
(
function
(){
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
var
advancedModel
=
new
CMS
.
Models
.
Settings
.
Advanced
(
$
{
advanced_dict
|
n
},
{
parse
:
true
});
advancedModel
.
blacklistKeys
=
$
{
advanced_blacklist
|
n
};
advancedModel
.
url
=
"${reverse('course_advanced_settings', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}"
;
var
settingsModel
=
new
CMS
.
Models
.
Settings
.
CourseSettings
({
courseLocation
:
new
CMS
.
Models
.
Location
(
'${context_course.location}'
,{
parse
:
true
}),
details
:
new
CMS
.
Models
.
Settings
.
CourseDetails
(
$
{
course_details
|
n
},{
parse
:
true
})
details
:
new
CMS
.
Models
.
Settings
.
CourseDetails
(
$
{
course_details
|
n
},{
parse
:
true
}),
advanced
:
advancedModel
});
var
advancedSettingsModel
=
new
CMS
.
Models
.
Settings
.
Advanced
({});
var
editor
=
new
CMS
.
Views
.
Settings
.
Main
({
el
:
$
(
'.main-wrapper'
),
model
:
settingsModel
...
...
@@ -743,90 +748,6 @@ from contentstore import utils
<!-- basic empty & initial empty field (if user had no values yet) -->
<ul
class=
"input-list course-advanced-policy-list"
>
<li
class=
"input multi course-advanced-policy-list-item"
>
<div
class=
"row"
>
<div
class=
"key"
>
<label
for=
"course-advanced-policy-1-key"
>
Policy Key:
</label>
<div
class=
"field"
>
<input
type=
"text"
class=
"short"
id=
"course-advanced-policy-1-key"
value=
""
/>
<span
class=
"tip tip-stacked"
>
Keys are case sensitive and cannot contain spaces or start with a number
</span>
</div>
</div>
<div
class=
"value"
>
<label
for=
"course-advanced-policy-1-value"
>
Policy Value:
</label>
<div
class=
"field"
>
<div
class=
"ace text"
id=
"course-advanced-policy-1-value"
>
some existing text
</div>
</div>
</div>
</div>
<a
href=
"#"
class=
"delete-button standard remove-item advanced-policy-data"
><span
class=
"delete-icon"
></span>
Delete
</a>
</li>
<!-- error existing key pair example -->
<li
class=
"input multi course-advanced-policy-list-item"
>
<div
class=
"row"
>
<div
class=
"key"
>
<label
for=
"course-advanced-policy-2-key"
>
Policy Key:
</label>
<div
class=
"field"
>
<input
type=
"text"
class=
"short"
id=
"course-advanced-policy-2-key"
value=
""
/>
<span
class=
"tip tip-stacked"
>
Keys are case sensitive and cannot contain spaces or start with a number
</span>
</div>
</div>
<div
class=
"value"
>
<label
for=
"course-advanced-policy-2-value"
>
Policy Value:
</label>
<div
class=
"field"
>
<textarea
class=
"ace text"
id=
"course-advanced-policy-2-value"
></textarea>
</div>
</div>
</div>
<span
class=
"message-error"
>
This policy key, $KEYNAME, already exists.
</span>
<a
href=
"#"
class=
"delete-button standard remove-item advanced-policy-data"
><span
class=
"delete-icon"
></span>
Delete
</a>
</li>
<!-- error on key left empty example -->
<li
class=
"input multi course-advanced-policy-list-item"
>
<div
class=
"row"
>
<div
class=
"key error"
>
<label
for=
"course-advanced-policy-3-key"
>
Policy Key:
</label>
<div
class=
"field"
>
<input
type=
"text"
class=
"short"
id=
"course-advanced-policy-3-key"
value=
""
/>
<span
class=
"tip tip-stacked"
>
Keys are case sensitive and cannot contain spaces or start with a number
</span>
</div>
</div>
<div
class=
"value error"
>
<label
for=
"course-advanced-policy-3-value"
>
Policy Value:
</label>
<div
class=
"field"
>
<textarea
class=
"ace text"
id=
"course-advanced-policy-3-value"
></textarea>
</div>
</div>
</div>
<span
class=
"message-error"
>
You cannot leave the key value for this pair blank.
</span>
<a
href=
"#"
class=
"delete-button standard remove-item advanced-policy-data"
><span
class=
"delete-icon"
></span>
Delete
</a>
</li>
<!-- error with value formatting example -->
<li
class=
"input multi course-advanced-policy-list-item"
>
<div
class=
"row"
>
<div
class=
"key error"
>
<label
for=
"course-advanced-policy-4-key"
>
Policy Key:
</label>
<div
class=
"field"
>
<input
type=
"text"
class=
"short"
id=
"course-advanced-policy-4-key"
value=
""
/>
<span
class=
"tip tip-stacked"
>
Keys are case sensitive and cannot contain spaces or start with a number
</span>
</div>
</div>
<div
class=
"value error"
>
<label
for=
"course-advanced-policy-4-value"
>
Policy Value:
</label>
<div
class=
"field"
>
<textarea
class=
"ace text"
id=
"course-advanced-policy-4-value"
value=
""
></textarea>
</div>
</div>
</div>
<span
class=
"message-error"
>
The JSON value for $KEYNAME is invalid.
</span>
<a
href=
"#"
class=
"delete-button standard remove-item advanced-policy-data"
><span
class=
"delete-icon"
></span>
Delete
</a>
</li>
</ul>
<!-- advanced policy actions -->
...
...
@@ -838,14 +759,6 @@ from contentstore import utils
</a>
</div>
<!-- advanced policy actions (with disabled save state) -->
<div
class=
"actions actions-advanced-policies"
>
<a
href=
"#"
class=
"save-button disabled"
>
Save
</a>
<a
href=
"#"
class=
"cancel-button"
>
Cancel
</a>
<a
href=
"#"
class=
"new-button new-advanced-policy-item add-policy-data"
>
<span
class=
"plus-icon white"
></span>
New Manual Policy
</a>
</div>
</div>
</div>
</section>
<!-- .settings-advanced-policies -->
...
...
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