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):
...
@@ -1109,6 +1109,8 @@ def get_course_settings(request, org, course, name):
return
render_to_response
(
'settings.html'
,
{
return
render_to_response
(
'settings.html'
,
{
'active_tab'
:
'settings'
,
'active_tab'
:
'settings'
,
'context_course'
:
course_module
,
'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
)
'course_details'
:
json
.
dumps
(
course_details
,
cls
=
CourseSettingsEncoder
)
})
})
...
@@ -1133,6 +1135,9 @@ def course_settings_updates(request, org, course, name, section):
...
@@ -1133,6 +1135,9 @@ def course_settings_updates(request, org, course, name, section):
manager
=
CourseDetails
manager
=
CourseDetails
elif
section
==
'grading'
:
elif
section
==
'grading'
:
manager
=
CourseGradingModel
manager
=
CourseGradingModel
elif
section
==
'advanced'
:
# not implemented b/c it assumes prefetched and then everything thru course_edit_metadata
return
else
:
return
else
:
return
if
request
.
method
==
'GET'
:
if
request
.
method
==
'GET'
:
...
@@ -1194,14 +1199,10 @@ def course_edit_metadata(request, org, course, name):
...
@@ -1194,14 +1199,10 @@ def course_edit_metadata(request, org, course, name):
editable
=
CourseMetadata
.
fetch
(
location
)
editable
=
CourseMetadata
.
fetch
(
location
)
return
render_to_response
(
'course_info.html'
,
{
# for now defer to settings general until we split the divs out into separate pages
'active_tab'
:
'settings'
,
return
get_course_settings
(
request
,
org
,
course
,
name
)
'editable_metadata'
:
editable
,
'url_base'
:
"/"
+
org
+
"/"
+
course
+
"/"
,
'blacklist_keys'
:
CourseMetadata
.
FILTERED_LIST
})
@expect_json
## NB: expect_json failed on ["key", "key2"] and json payload
@login_required
@login_required
@ensure_csrf_cookie
@ensure_csrf_cookie
def
course_metadata_rest_access
(
request
,
org
,
course
,
name
):
def
course_metadata_rest_access
(
request
,
org
,
course
,
name
):
...
@@ -1225,10 +1226,11 @@ 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'
:
if
request
.
method
==
'GET'
:
return
HttpResponse
(
json
.
dumps
(
CourseMetadata
.
fetch
(
location
)),
mimetype
=
"application/json"
)
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
elif
real_method
==
'DELETE'
:
return
HttpResponse
(
json
.
dumps
(
CourseMetadata
.
delete_key
(
location
,
request
.
POST
)),
mimetype
=
"application/json"
)
return
HttpResponse
(
json
.
dumps
(
CourseMetadata
.
delete_key
(
location
,
json
.
loads
(
request
.
body
)
)),
mimetype
=
"application/json"
)
elif
request
.
method
==
'POST'
:
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
@login_required
...
...
cms/djangoapps/models/settings/course_metadata.py
View file @
8f16d639
...
@@ -7,8 +7,8 @@ class CourseMetadata(object):
...
@@ -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.
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.
The objects have no predefined attrs but instead are obj encodings of the editable metadata.
'''
'''
# __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'
]
FILTERED_LIST
=
[
'start'
,
'end'
,
'enrollment_start'
,
'enrollment_end'
,
'tabs'
,
'graceperiod'
,
'__new_advanced_key__'
]
@classmethod
@classmethod
def
fetch
(
cls
,
course_location
):
def
fetch
(
cls
,
course_location
):
...
@@ -57,14 +57,10 @@ class CourseMetadata(object):
...
@@ -57,14 +57,10 @@ class CourseMetadata(object):
'''
'''
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
descriptor
=
get_modulestore
(
course_location
)
.
get_item
(
course_location
)
if
isinstance
(
payload
,
list
):
for
key
in
payload
[
'deleteKeys'
]:
for
key
in
payload
:
if
key
in
descriptor
.
metadata
:
if
key
in
descriptor
.
metadata
:
del
descriptor
.
metadata
[
key
]
del
descriptor
.
metadata
[
key
]
else
:
if
payload
in
descriptor
.
metadata
:
del
descriptor
.
metadata
[
payload
]
get_modulestore
(
course_location
)
.
update_metadata
(
course_location
,
descriptor
.
metadata
)
get_modulestore
(
course_location
)
.
update_metadata
(
course_location
,
descriptor
.
metadata
)
return
cls
.
fetch
(
course_location
)
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 = {};
...
@@ -2,13 +2,211 @@ if (!CMS.Models['Settings']) CMS.Models.Settings = {};
CMS
.
Models
.
Settings
.
Advanced
=
Backbone
.
Model
.
extend
({
CMS
.
Models
.
Settings
.
Advanced
=
Backbone
.
Model
.
extend
({
defaults
:
{
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
()
{
initialize
:
function
()
{
console
.
log
(
'in initialize'
);
console
.
log
(
'in initialize'
);
var
editor
=
ace
.
edit
(
'course-advanced-policy-1-value'
);
},
editor
.
setTheme
(
"ace/theme/monokai"
);
validate
:
function
(
attrs
)
{
editor
.
getSession
().
setMode
(
"ace/mode/javascript"
);
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
();
if
(
!
CMS
.
Models
[
'Settings'
])
CMS
.
Models
.
Settings
=
new
Object
();
CMS
.
Models
.
Settings
.
CourseGradingPolicy
=
Backbone
.
Model
.
extend
({
CMS
.
Models
.
Settings
.
CourseGradingPolicy
=
Backbone
.
Model
.
extend
({
defaults
:
{
defaults
:
{
course_location
:
null
,
course_location
:
null
,
graders
:
null
,
// CourseGraderCollection
graders
:
null
,
// CourseGraderCollection
grade_cutoffs
:
null
,
// CourseGradeCutoff model
grade_cutoffs
:
null
,
// CourseGradeCutoff model
grace_period
:
null
// either null or { hours: n, minutes: m, ...}
grace_period
:
null
// either null or { hours: n, minutes: m, ...}
},
},
parse
:
function
(
attributes
)
{
parse
:
function
(
attributes
)
{
if
(
attributes
[
'course_location'
])
{
if
(
attributes
[
'course_location'
])
{
attributes
.
course_location
=
new
CMS
.
Models
.
Location
(
attributes
.
course_location
,
{
parse
:
true
});
attributes
.
course_location
=
new
CMS
.
Models
.
Location
(
attributes
.
course_location
,
{
parse
:
true
});
}
}
if
(
attributes
[
'graders'
])
{
if
(
attributes
[
'graders'
])
{
var
graderCollection
;
var
graderCollection
;
if
(
this
.
has
(
'graders'
))
{
if
(
this
.
has
(
'graders'
))
{
graderCollection
=
this
.
get
(
'graders'
);
graderCollection
=
this
.
get
(
'graders'
);
graderCollection
.
reset
(
attributes
.
graders
);
graderCollection
.
reset
(
attributes
.
graders
);
}
}
else
{
else
{
graderCollection
=
new
CMS
.
Models
.
Settings
.
CourseGraderCollection
(
attributes
.
graders
);
graderCollection
=
new
CMS
.
Models
.
Settings
.
CourseGraderCollection
(
attributes
.
graders
);
graderCollection
.
course_location
=
attributes
[
'course_location'
]
||
this
.
get
(
'course_location'
);
graderCollection
.
course_location
=
attributes
[
'course_location'
]
||
this
.
get
(
'course_location'
);
}
}
attributes
.
graders
=
graderCollection
;
attributes
.
graders
=
graderCollection
;
}
}
return
attributes
;
return
attributes
;
},
},
url
:
function
()
{
url
:
function
()
{
var
location
=
this
.
get
(
'course_location'
);
var
location
=
this
.
get
(
'course_location'
);
return
'/'
+
location
.
get
(
'org'
)
+
"/"
+
location
.
get
(
'course'
)
+
'/settings/'
+
location
.
get
(
'name'
)
+
'/section/grading'
;
return
'/'
+
location
.
get
(
'org'
)
+
"/"
+
location
.
get
(
'course'
)
+
'/settings/'
+
location
.
get
(
'name'
)
+
'/section/grading'
;
},
},
gracePeriodToDate
:
function
()
{
gracePeriodToDate
:
function
()
{
var
newDate
=
new
Date
();
var
newDate
=
new
Date
();
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'hours'
])
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'hours'
])
newDate
.
setHours
(
this
.
get
(
'grace_period'
)[
'hours'
]);
newDate
.
setHours
(
this
.
get
(
'grace_period'
)[
'hours'
]);
else
newDate
.
setHours
(
0
);
else
newDate
.
setHours
(
0
);
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'minutes'
])
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'minutes'
])
newDate
.
setMinutes
(
this
.
get
(
'grace_period'
)[
'minutes'
]);
newDate
.
setMinutes
(
this
.
get
(
'grace_period'
)[
'minutes'
]);
else
newDate
.
setMinutes
(
0
);
else
newDate
.
setMinutes
(
0
);
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'seconds'
])
if
(
this
.
has
(
'grace_period'
)
&&
this
.
get
(
'grace_period'
)[
'seconds'
])
newDate
.
setSeconds
(
this
.
get
(
'grace_period'
)[
'seconds'
]);
newDate
.
setSeconds
(
this
.
get
(
'grace_period'
)[
'seconds'
]);
else
newDate
.
setSeconds
(
0
);
else
newDate
.
setSeconds
(
0
);
return
newDate
;
return
newDate
;
},
},
dateToGracePeriod
:
function
(
date
)
{
dateToGracePeriod
:
function
(
date
)
{
return
{
hours
:
date
.
getHours
(),
minutes
:
date
.
getMinutes
(),
seconds
:
date
.
getSeconds
()
};
return
{
hours
:
date
.
getHours
(),
minutes
:
date
.
getMinutes
(),
seconds
:
date
.
getSeconds
()
};
}
}
});
});
CMS
.
Models
.
Settings
.
CourseGrader
=
Backbone
.
Model
.
extend
({
CMS
.
Models
.
Settings
.
CourseGrader
=
Backbone
.
Model
.
extend
({
defaults
:
{
defaults
:
{
"type"
:
""
,
// must be unique w/in collection (ie. w/in course)
"type"
:
""
,
// must be unique w/in collection (ie. w/in course)
"min_count"
:
1
,
"min_count"
:
1
,
"drop_count"
:
0
,
"drop_count"
:
0
,
...
@@ -57,71 +57,71 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
...
@@ -57,71 +57,71 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
"weight"
:
0
// int 0..100
"weight"
:
0
// int 0..100
},
},
parse
:
function
(
attrs
)
{
parse
:
function
(
attrs
)
{
if
(
attrs
[
'weight'
])
{
if
(
attrs
[
'weight'
])
{
if
(
!
_
.
isNumber
(
attrs
.
weight
))
attrs
.
weight
=
parseInt
(
attrs
.
weight
);
if
(
!
_
.
isNumber
(
attrs
.
weight
))
attrs
.
weight
=
parseInt
(
attrs
.
weight
);
}
}
if
(
attrs
[
'min_count'
])
{
if
(
attrs
[
'min_count'
])
{
if
(
!
_
.
isNumber
(
attrs
.
min_count
))
attrs
.
min_count
=
parseInt
(
attrs
.
min_count
);
if
(
!
_
.
isNumber
(
attrs
.
min_count
))
attrs
.
min_count
=
parseInt
(
attrs
.
min_count
);
}
}
if
(
attrs
[
'drop_count'
])
{
if
(
attrs
[
'drop_count'
])
{
if
(
!
_
.
isNumber
(
attrs
.
drop_count
))
attrs
.
drop_count
=
parseInt
(
attrs
.
drop_count
);
if
(
!
_
.
isNumber
(
attrs
.
drop_count
))
attrs
.
drop_count
=
parseInt
(
attrs
.
drop_count
);
}
}
return
attrs
;
return
attrs
;
},
},
validate
:
function
(
attrs
)
{
validate
:
function
(
attrs
)
{
var
errors
=
{};
var
errors
=
{};
if
(
attrs
[
'type'
])
{
if
(
attrs
[
'type'
])
{
if
(
_
.
isEmpty
(
attrs
[
'type'
]))
{
if
(
_
.
isEmpty
(
attrs
[
'type'
]))
{
errors
.
type
=
"The assignment type must have a name."
;
errors
.
type
=
"The assignment type must have a name."
;
}
}
else
{
else
{
// FIXME somehow this.collection is unbound sometimes. I can't track down when
// 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
);
var
existing
=
this
.
collection
&&
this
.
collection
.
some
(
function
(
other
)
{
return
(
other
!=
this
)
&&
(
other
.
get
(
'type'
)
==
attrs
[
'type'
]);},
this
);
if
(
existing
)
{
if
(
existing
)
{
errors
.
type
=
"There's already another assignment type with this name."
;
errors
.
type
=
"There's already another assignment type with this name."
;
}
}
}
}
}
}
if
(
attrs
[
'weight'
])
{
if
(
attrs
[
'weight'
])
{
if
(
!
isFinite
(
attrs
.
weight
)
||
/
\D
+/
.
test
(
attrs
.
weight
))
{
if
(
!
isFinite
(
attrs
.
weight
)
||
/
\D
+/
.
test
(
attrs
.
weight
))
{
errors
.
weight
=
"Please enter an integer between 0 and 100."
;
errors
.
weight
=
"Please enter an integer between 0 and 100."
;
}
}
else
{
else
{
attrs
.
weight
=
parseInt
(
attrs
.
weight
);
// see if this ensures value saved is int
attrs
.
weight
=
parseInt
(
attrs
.
weight
);
// see if this ensures value saved is int
if
(
this
.
collection
&&
attrs
.
weight
>
0
)
{
if
(
this
.
collection
&&
attrs
.
weight
>
0
)
{
// FIXME b/c saves don't update the models if validation fails, we should
// 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
// 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
// or figure out a wholistic way to balance the vals across the whole
//
if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100)
//
if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100)
//
errors.weight = "The weights cannot add to more than 100.";
//
errors.weight = "The weights cannot add to more than 100.";
}
}
}}
}}
if
(
attrs
[
'min_count'
])
{
if
(
attrs
[
'min_count'
])
{
if
(
!
isFinite
(
attrs
.
min_count
)
||
/
\D
+/
.
test
(
attrs
.
min_count
))
{
if
(
!
isFinite
(
attrs
.
min_count
)
||
/
\D
+/
.
test
(
attrs
.
min_count
))
{
errors
.
min_count
=
"Please enter an integer."
;
errors
.
min_count
=
"Please enter an integer."
;
}
}
else
attrs
.
min_count
=
parseInt
(
attrs
.
min_count
);
else
attrs
.
min_count
=
parseInt
(
attrs
.
min_count
);
}
}
if
(
attrs
[
'drop_count'
])
{
if
(
attrs
[
'drop_count'
])
{
if
(
!
isFinite
(
attrs
.
drop_count
)
||
/
\D
+/
.
test
(
attrs
.
drop_count
))
{
if
(
!
isFinite
(
attrs
.
drop_count
)
||
/
\D
+/
.
test
(
attrs
.
drop_count
))
{
errors
.
drop_count
=
"Please enter an integer."
;
errors
.
drop_count
=
"Please enter an integer."
;
}
}
else
attrs
.
drop_count
=
parseInt
(
attrs
.
drop_count
);
else
attrs
.
drop_count
=
parseInt
(
attrs
.
drop_count
);
}
}
if
(
attrs
[
'min_count'
]
&&
attrs
[
'drop_count'
]
&&
attrs
.
drop_count
>
attrs
.
min_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."
;
errors
.
drop_count
=
"Cannot drop more "
+
attrs
.
type
+
" than will assigned."
;
}
}
if
(
!
_
.
isEmpty
(
errors
))
return
errors
;
if
(
!
_
.
isEmpty
(
errors
))
return
errors
;
}
}
});
});
CMS
.
Models
.
Settings
.
CourseGraderCollection
=
Backbone
.
Collection
.
extend
({
CMS
.
Models
.
Settings
.
CourseGraderCollection
=
Backbone
.
Collection
.
extend
({
model
:
CMS
.
Models
.
Settings
.
CourseGrader
,
model
:
CMS
.
Models
.
Settings
.
CourseGrader
,
course_location
:
null
,
// must be set to a Location object
course_location
:
null
,
// must be set to a Location object
url
:
function
()
{
url
:
function
()
{
return
'/'
+
this
.
course_location
.
get
(
'org'
)
+
"/"
+
this
.
course_location
.
get
(
'course'
)
+
'/grades/'
+
this
.
course_location
.
get
(
'name'
)
+
'/'
;
return
'/'
+
this
.
course_location
.
get
(
'org'
)
+
"/"
+
this
.
course_location
.
get
(
'course'
)
+
'/grades/'
+
this
.
course_location
.
get
(
'name'
)
+
'/'
;
},
},
sumWeights
:
function
()
{
sumWeights
:
function
()
{
return
this
.
reduce
(
function
(
subtotal
,
grader
)
{
return
subtotal
+
grader
.
get
(
'weight'
);
},
0
);
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 @@
...
@@ -5,7 +5,7 @@
if
(
typeof
window
.
templateLoader
==
'function'
)
return
;
if
(
typeof
window
.
templateLoader
==
'function'
)
return
;
var
templateLoader
=
{
var
templateLoader
=
{
templateVersion
:
"0.0.1
2
"
,
templateVersion
:
"0.0.1
3
"
,
templates
:
{},
templates
:
{},
loadRemoteTemplate
:
function
(
templateName
,
filename
,
callback
)
{
loadRemoteTemplate
:
function
(
templateName
,
filename
,
callback
)
{
if
(
!
this
.
templates
[
templateName
])
{
if
(
!
this
.
templates
[
templateName
])
{
...
...
cms/static/js/views/settings/main_settings_view.js
View file @
8f16d639
if
(
!
CMS
.
Views
[
'Settings'
])
CMS
.
Views
.
Settings
=
{};
if
(
!
CMS
.
Views
[
'Settings'
])
CMS
.
Views
.
Settings
=
{};
//
TODO move to common place
//TODO move to common place
CMS
.
Views
.
ValidatingView
=
Backbone
.
View
.
extend
({
CMS
.
Views
.
ValidatingView
=
Backbone
.
View
.
extend
({
// Intended as an abstract class which catches validation errors on the model and
// 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
// decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents
// either have your init call this one or copy the contents
initialize
:
function
()
{
initialize
:
function
()
{
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
},
},
errorTemplate
:
_
.
template
(
'<span class="message-error"><%= message %></span>'
),
errorTemplate
:
_
.
template
(
'<span class="message-error"><%= message %></span>'
),
events
:
{
events
:
{
"blur input"
:
"clearValidationErrors"
,
"change input"
:
"clearValidationErrors"
,
"blur textarea"
:
"clearValidationErrors"
"change textarea"
:
"clearValidationErrors"
},
},
fieldToSelectorMap
:
{
fieldToSelectorMap
:
{
// Your subclass must populate this w/ all of the model keys and dom selectors
// Your subclass must populate this w/ all of the model keys and dom selectors
// which may be the subjects of validation errors
// which may be the subjects of validation errors
},
},
_cacheValidationErrors
:
[],
_cacheValidationErrors
:
[],
handleValidationError
:
function
(
model
,
error
)
{
handleValidationError
:
function
(
model
,
error
)
{
// error is object w/ fields and error strings
console
.
log
(
'validation'
,
model
,
error
);
for
(
var
field
in
error
)
{
// error is object w/ fields and error strings
var
ele
=
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
field
]);
for
(
var
field
in
error
)
{
this
.
_cacheValidationErrors
.
push
(
ele
);
var
ele
=
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
field
]);
if
(
$
(
ele
).
is
(
'div'
))
{
this
.
_cacheValidationErrors
.
push
(
ele
);
// put error on the contained inputs
if
(
$
(
ele
).
is
(
'div'
))
{
$
(
ele
).
find
(
'input, textarea'
).
addClass
(
'error'
);
// put error on the contained inputs
}
$
(
ele
).
find
(
'input, textarea'
).
addClass
(
'error'
);
else
$
(
ele
).
addClass
(
'error'
);
}
$
(
ele
).
parent
().
append
(
this
.
errorTemplate
({
message
:
error
[
field
]}));
else
$
(
ele
).
addClass
(
'error'
);
}
$
(
ele
).
parent
().
append
(
this
.
errorTemplate
({
message
:
error
[
field
]}));
},
}
},
clearValidationErrors
:
function
()
{
// error is object w/ fields and error strings
clearValidationErrors
:
function
()
{
while
(
this
.
_cacheValidationErrors
.
length
>
0
)
{
// error is object w/ fields and error strings
var
ele
=
this
.
_cacheValidationErrors
.
pop
();
while
(
this
.
_cacheValidationErrors
.
length
>
0
)
{
if
(
$
(
ele
).
is
(
'div'
))
{
var
ele
=
this
.
_cacheValidationErrors
.
pop
();
// put error on the contained inputs
if
(
$
(
ele
).
is
(
'div'
))
{
$
(
ele
).
find
(
'input, textarea'
).
removeClass
(
'error'
);
// put error on the contained inputs
}
$
(
ele
).
find
(
'input, textarea'
).
removeClass
(
'error'
);
else
$
(
ele
).
removeClass
(
'error'
);
}
$
(
ele
).
nextAll
(
'.message-error'
).
remove
();
else
$
(
ele
).
removeClass
(
'error'
);
}
$
(
ele
).
nextAll
(
'.message-error'
).
remove
();
},
}
},
saveIfChanged
:
function
(
event
)
{
// returns true if the value changed and was thus sent to server
saveIfChanged
:
function
(
event
)
{
var
field
=
this
.
selectorToField
[
event
.
currentTarget
.
id
];
// returns true if the value changed and was thus sent to server
var
currentVal
=
this
.
model
.
get
(
field
);
var
field
=
this
.
selectorToField
[
event
.
currentTarget
.
id
];
var
newVal
=
$
(
event
.
currentTarget
).
val
();
var
currentVal
=
this
.
model
.
get
(
field
);
if
(
currentVal
!=
newVal
)
{
var
newVal
=
$
(
event
.
currentTarget
).
val
();
this
.
clearValidationErrors
();
if
(
currentVal
!=
newVal
)
{
this
.
model
.
save
(
field
,
newVal
,
{
error
:
CMS
.
ServerError
});
this
.
clearValidationErrors
();
return
true
;
this
.
model
.
save
(
field
,
newVal
,
{
error
:
CMS
.
ServerError
});
}
return
true
;
else
return
false
;
}
}
else
return
false
;
}
});
});
CMS
.
Views
.
Settings
.
Main
=
Backbone
.
View
.
extend
({
CMS
.
Views
.
Settings
.
Main
=
Backbone
.
View
.
extend
({
// Model class is CMS.Models.Settings.CourseSettings
// Model class is CMS.Models.Settings.CourseSettings
// allow navigation between the tabs
// allow navigation between the tabs
events
:
{
events
:
{
'click .settings-page-menu a'
:
"showSettingsTab"
,
'click .settings-page-menu a'
:
"showSettingsTab"
,
'mouseover #timezone'
:
"updateTime"
'mouseover #timezone'
:
"updateTime"
},
},
currentTab
:
null
,
currentTab
:
null
,
subviews
:
{},
// indexed by tab name
subviews
:
{},
// indexed by tab name
initialize
:
function
()
{
initialize
:
function
()
{
// load templates
// load templates
this
.
currentTab
=
this
.
$el
.
find
(
'.settings-page-menu .is-shown'
).
attr
(
'data-section'
);
this
.
currentTab
=
this
.
$el
.
find
(
'.settings-page-menu .is-shown'
).
attr
(
'data-section'
);
// create the initial subview
// create the initial subview
this
.
subviews
[
this
.
currentTab
]
=
this
.
createSubview
();
this
.
subviews
[
this
.
currentTab
]
=
this
.
createSubview
();
// fill in fields
// fill in fields
this
.
$el
.
find
(
"#course-name"
).
val
(
this
.
model
.
get
(
'courseLocation'
).
get
(
'name'
));
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-organization"
).
val
(
this
.
model
.
get
(
'courseLocation'
).
get
(
'org'
));
this
.
$el
.
find
(
"#course-number"
).
val
(
this
.
model
.
get
(
'courseLocation'
).
get
(
'course'
));
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
(
'.set-date'
).
datepicker
({
'dateFormat'
:
'm/d/yy'
});
this
.
$el
.
find
(
":input, textarea"
).
focus
(
function
()
{
this
.
$el
.
find
(
":input, textarea"
).
focus
(
function
()
{
$
(
"label[for='"
+
this
.
id
+
"']"
).
addClass
(
"is-focused"
);
$
(
"label[for='"
+
this
.
id
+
"']"
).
addClass
(
"is-focused"
);
}).
blur
(
function
()
{
}).
blur
(
function
()
{
$
(
"label"
).
removeClass
(
"is-focused"
);
$
(
"label"
).
removeClass
(
"is-focused"
);
});
});
this
.
render
();
this
.
render
();
},
},
render
:
function
()
{
render
:
function
()
{
// create any necessary subviews and put them onto the page
// create any necessary subviews and put them onto the page
if
(
!
this
.
model
.
has
(
this
.
currentTab
))
{
if
(
!
this
.
model
.
has
(
this
.
currentTab
))
{
// TODO disable screen until fetch completes?
// TODO disable screen until fetch completes?
var
cachethis
=
this
;
var
cachethis
=
this
;
this
.
model
.
retrieve
(
this
.
currentTab
,
function
()
{
this
.
model
.
retrieve
(
this
.
currentTab
,
function
()
{
cachethis
.
subviews
[
cachethis
.
currentTab
]
=
cachethis
.
createSubview
();
cachethis
.
subviews
[
cachethis
.
currentTab
]
=
cachethis
.
createSubview
();
cachethis
.
subviews
[
cachethis
.
currentTab
].
render
();
cachethis
.
subviews
[
cachethis
.
currentTab
].
render
();
});
});
}
}
else
this
.
subviews
[
this
.
currentTab
].
render
();
else
{
// Advanced (at least) model gets created at bootstrap but the view does not
var
dateIntrospect
=
new
Date
();
if
(
!
this
.
subviews
[
this
.
currentTab
])
{
this
.
$el
.
find
(
'#timezone'
).
html
(
"("
+
dateIntrospect
.
getTimezone
()
+
")"
);
this
.
subviews
[
this
.
currentTab
]
=
this
.
createSubview
();
}
return
this
;
this
.
subviews
[
this
.
currentTab
].
render
();
},
}
createSubview
:
function
()
{
var
dateIntrospect
=
new
Date
();
switch
(
this
.
currentTab
)
{
this
.
$el
.
find
(
'#timezone'
).
html
(
"("
+
dateIntrospect
.
getTimezone
()
+
")"
);
case
'details'
:
return
new
CMS
.
Views
.
Settings
.
Details
({
return
this
;
el
:
this
.
$el
.
find
(
'.settings-'
+
this
.
currentTab
),
},
model
:
this
.
model
.
get
(
this
.
currentTab
)
});
createSubview
:
function
()
{
case
'faculty'
:
switch
(
this
.
currentTab
)
{
break
;
case
'details'
:
case
'grading'
:
return
new
CMS
.
Views
.
Settings
.
Details
({
return
new
CMS
.
Views
.
Settings
.
Grading
({
el
:
this
.
$el
.
find
(
'.settings-'
+
this
.
currentTab
),
el
:
this
.
$el
.
find
(
'.settings-'
+
this
.
currentTab
),
model
:
this
.
model
.
get
(
this
.
currentTab
)
model
:
this
.
model
.
get
(
this
.
currentTab
)
});
});
break
;
case
'problems'
:
case
'faculty'
:
break
;
break
;
case
'discussions'
:
case
'grading'
:
break
;
return
new
CMS
.
Views
.
Settings
.
Grading
({
}
el
:
this
.
$el
.
find
(
'.settings-'
+
this
.
currentTab
),
},
model
:
this
.
model
.
get
(
this
.
currentTab
)
});
updateTime
:
function
(
e
)
{
break
;
var
now
=
new
Date
();
case
'advanced'
:
var
hours
=
now
.
getHours
();
return
new
CMS
.
Views
.
Settings
.
Advanced
({
var
minutes
=
now
.
getMinutes
();
el
:
this
.
$el
.
find
(
'.settings-'
+
this
.
currentTab
),
$
(
e
.
currentTarget
).
attr
(
'title'
,
(
hours
%
12
===
0
?
12
:
hours
%
12
)
+
":"
+
(
minutes
<
10
?
"0"
:
""
)
+
model
:
this
.
model
.
get
(
this
.
currentTab
)
now
.
getMinutes
()
+
(
hours
<
12
?
"am"
:
"pm"
)
+
" (current local time)"
);
});
},
break
;
case
'problems'
:
showSettingsTab
:
function
(
e
)
{
break
;
this
.
currentTab
=
$
(
e
.
target
).
attr
(
'data-section'
);
case
'discussions'
:
$
(
'.settings-page-section > section'
).
hide
();
break
;
$
(
'.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
updateTime
:
function
(
e
)
{
this
.
render
();
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
({
CMS
.
Views
.
Settings
.
Details
=
CMS
.
Views
.
ValidatingView
.
extend
({
// Model class is CMS.Models.Settings.CourseDetails
// Model class is CMS.Models.Settings.CourseDetails
events
:
{
events
:
{
"blur
input"
:
"updateModel"
,
"change
input"
:
"updateModel"
,
"blur
textarea"
:
"updateModel"
,
"change
textarea"
:
"updateModel"
,
'click .remove-course-syllabus'
:
"removeSyllabus"
,
'click .remove-course-syllabus'
:
"removeSyllabus"
,
'click .new-course-syllabus'
:
'assetSyllabus'
,
'click .new-course-syllabus'
:
'assetSyllabus'
,
'click .remove-course-introduction-video'
:
"removeVideo"
,
'click .remove-course-introduction-video'
:
"removeVideo"
,
'focus #course-overview'
:
"codeMirrorize"
'focus #course-overview'
:
"codeMirrorize"
},
},
initialize
:
function
()
{
initialize
:
function
()
{
// TODO move the html frag to a loaded asset
// 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
.
fileAnchorTemplate
=
_
.
template
(
'<a href="<%= fullpath %>"> <i class="ss-icon ss-standard">📄</i><%= filename %></a>'
);
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
},
},
render
:
function
()
{
render
:
function
()
{
this
.
setupDatePicker
(
'start_date'
);
this
.
setupDatePicker
(
'start_date'
);
this
.
setupDatePicker
(
'end_date'
);
this
.
setupDatePicker
(
'end_date'
);
this
.
setupDatePicker
(
'enrollment_start'
);
this
.
setupDatePicker
(
'enrollment_start'
);
this
.
setupDatePicker
(
'enrollment_end'
);
this
.
setupDatePicker
(
'enrollment_end'
);
if
(
this
.
model
.
has
(
'syllabus'
))
{
if
(
this
.
model
.
has
(
'syllabus'
))
{
this
.
$el
.
find
(
this
.
fieldToSelectorMap
[
'syllabus'
]).
html
(
this
.
$el
.
find
(
this
.
fieldToSelectorMap
[
'syllabus'
]).
html
(
this
.
fileAnchorTemplate
({
this
.
fileAnchorTemplate
({
fullpath
:
this
.
model
.
get
(
'syllabus'
),
fullpath
:
this
.
model
.
get
(
'syllabus'
),
filename
:
'syllabus'
}));
filename
:
'syllabus'
}));
this
.
$el
.
find
(
'.remove-course-syllabus'
).
show
();
this
.
$el
.
find
(
'.remove-course-syllabus'
).
show
();
}
}
else
{
else
{
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'syllabus'
]).
html
(
""
);
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'syllabus'
]).
html
(
""
);
this
.
$el
.
find
(
'.remove-course-syllabus'
).
hide
();
this
.
$el
.
find
(
'.remove-course-syllabus'
).
hide
();
}
}
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'overview'
]).
val
(
this
.
model
.
get
(
'overview'
));
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'overview'
]).
val
(
this
.
model
.
get
(
'overview'
));
this
.
codeMirrorize
(
null
,
$
(
'#course-overview'
)[
0
]);
this
.
codeMirrorize
(
null
,
$
(
'#course-overview'
)[
0
]);
this
.
$el
.
find
(
'.current-course-introduction-video iframe'
).
attr
(
'src'
,
this
.
model
.
videosourceSample
());
this
.
$el
.
find
(
'.current-course-introduction-video iframe'
).
attr
(
'src'
,
this
.
model
.
videosourceSample
());
if
(
this
.
model
.
has
(
'intro_video'
))
{
if
(
this
.
model
.
has
(
'intro_video'
))
{
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
show
();
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
show
();
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'intro_video'
]).
val
(
this
.
model
.
get
(
'intro_video'
));
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'intro_video'
]).
val
(
this
.
model
.
get
(
'intro_video'
));
}
}
else
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
else
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'effort'
]).
val
(
this
.
model
.
get
(
'effort'
));
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'effort'
]).
val
(
this
.
model
.
get
(
'effort'
));
return
this
;
return
this
;
},
},
fieldToSelectorMap
:
{
fieldToSelectorMap
:
{
'start_date'
:
"course-start"
,
'start_date'
:
"course-start"
,
'end_date'
:
'course-end'
,
'end_date'
:
'course-end'
,
'enrollment_start'
:
'enrollment-start'
,
'enrollment_start'
:
'enrollment-start'
,
'enrollment_end'
:
'enrollment-end'
,
'enrollment_end'
:
'enrollment-end'
,
'syllabus'
:
'.current-course-syllabus .doc-filename'
,
'syllabus'
:
'.current-course-syllabus .doc-filename'
,
'overview'
:
'course-overview'
,
'overview'
:
'course-overview'
,
'intro_video'
:
'course-introduction-video'
,
'intro_video'
:
'course-introduction-video'
,
'effort'
:
"course-effort"
'effort'
:
"course-effort"
},
},
setupDatePicker
:
function
(
fieldName
)
{
setupDatePicker
:
function
(
fieldName
)
{
var
cacheModel
=
this
.
model
;
var
cacheModel
=
this
.
model
;
...
@@ -245,58 +260,58 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
...
@@ -245,58 +260,58 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
datefield
.
datepicker
(
'setDate'
,
this
.
model
.
get
(
fieldName
));
datefield
.
datepicker
(
'setDate'
,
this
.
model
.
get
(
fieldName
));
if
(
this
.
model
.
has
(
fieldName
))
timefield
.
timepicker
(
'setTime'
,
this
.
model
.
get
(
fieldName
));
if
(
this
.
model
.
has
(
fieldName
))
timefield
.
timepicker
(
'setTime'
,
this
.
model
.
get
(
fieldName
));
},
},
updateModel
:
function
(
event
)
{
updateModel
:
function
(
event
)
{
switch
(
event
.
currentTarget
.
id
)
{
switch
(
event
.
currentTarget
.
id
)
{
case
'course-start-date'
:
// handled via onSelect method
case
'course-start-date'
:
// handled via onSelect method
case
'course-end-date'
:
case
'course-end-date'
:
case
'course-enrollment-start-date'
:
case
'course-enrollment-start-date'
:
case
'course-enrollment-end-date'
:
case
'course-enrollment-end-date'
:
break
;
break
;
case
'course-overview'
:
case
'course-overview'
:
// handled via code mirror
// handled via code mirror
break
;
break
;
case
'course-effort'
:
case
'course-effort'
:
this
.
saveIfChanged
(
event
);
this
.
saveIfChanged
(
event
);
break
;
break
;
case
'course-introduction-video'
:
case
'course-introduction-video'
:
this
.
clearValidationErrors
();
this
.
clearValidationErrors
();
var
previewsource
=
this
.
model
.
save_videosource
(
$
(
event
.
currentTarget
).
val
());
var
previewsource
=
this
.
model
.
save_videosource
(
$
(
event
.
currentTarget
).
val
());
this
.
$el
.
find
(
".current-course-introduction-video iframe"
).
attr
(
"src"
,
previewsource
);
this
.
$el
.
find
(
".current-course-introduction-video iframe"
).
attr
(
"src"
,
previewsource
);
if
(
this
.
model
.
has
(
'intro_video'
))
{
if
(
this
.
model
.
has
(
'intro_video'
))
{
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
show
();
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
show
();
}
}
else
{
else
{
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
}
}
break
;
break
;
default
:
default
:
break
;
break
;
}
}
},
},
removeSyllabus
:
function
()
{
removeSyllabus
:
function
()
{
if
(
this
.
model
.
has
(
'syllabus'
))
this
.
model
.
save
({
'syllabus'
:
null
},
if
(
this
.
model
.
has
(
'syllabus'
))
this
.
model
.
save
({
'syllabus'
:
null
},
{
error
:
CMS
.
ServerError
});
{
error
:
CMS
.
ServerError
});
},
},
assetSyllabus
:
function
()
{
assetSyllabus
:
function
()
{
// TODO implement
// TODO implement
},
},
removeVideo
:
function
()
{
removeVideo
:
function
()
{
if
(
this
.
model
.
has
(
'intro_video'
))
{
if
(
this
.
model
.
has
(
'intro_video'
))
{
this
.
model
.
save_videosource
(
null
);
this
.
model
.
save_videosource
(
null
);
this
.
$el
.
find
(
".current-course-introduction-video iframe"
).
attr
(
"src"
,
""
);
this
.
$el
.
find
(
".current-course-introduction-video iframe"
).
attr
(
"src"
,
""
);
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'intro_video'
]).
val
(
""
);
this
.
$el
.
find
(
'#'
+
this
.
fieldToSelectorMap
[
'intro_video'
]).
val
(
""
);
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
this
.
$el
.
find
(
'.remove-course-introduction-video'
).
hide
();
}
}
},
},
codeMirrors
:
{},
codeMirrors
:
{},
codeMirrorize
:
function
(
e
,
forcedTarget
)
{
codeMirrorize
:
function
(
e
,
forcedTarget
)
{
var
thisTarget
;
var
thisTarget
;
if
(
forcedTarget
)
{
if
(
forcedTarget
)
{
...
@@ -316,42 +331,42 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
...
@@ -316,42 +331,42 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
cachethis
.
clearValidationErrors
();
cachethis
.
clearValidationErrors
();
var
newVal
=
mirror
.
getValue
();
var
newVal
=
mirror
.
getValue
();
if
(
cachethis
.
model
.
get
(
field
)
!=
newVal
)
cachethis
.
model
.
save
(
field
,
newVal
,
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
({
CMS
.
Views
.
Settings
.
Grading
=
CMS
.
Views
.
ValidatingView
.
extend
({
// Model class is CMS.Models.Settings.CourseGradingPolicy
// Model class is CMS.Models.Settings.CourseGradingPolicy
events
:
{
events
:
{
"blur
input"
:
"updateModel"
,
"change
input"
:
"updateModel"
,
"blur
textarea"
:
"updateModel"
,
"change
textarea"
:
"updateModel"
,
"blur
span[contenteditable=true]"
:
"updateDesignation"
,
"change
span[contenteditable=true]"
:
"updateDesignation"
,
"click .settings-extra header"
:
"showSettingsExtras"
,
"click .settings-extra header"
:
"showSettingsExtras"
,
"click .new-grade-button"
:
"addNewGrade"
,
"click .new-grade-button"
:
"addNewGrade"
,
"click .remove-button"
:
"removeGrade"
,
"click .remove-button"
:
"removeGrade"
,
"click .add-grading-data"
:
"addAssignmentType"
"click .add-grading-data"
:
"addAssignmentType"
},
},
initialize
:
function
()
{
initialize
:
function
()
{
// load template for grading view
// load template for grading view
var
self
=
this
;
var
self
=
this
;
this
.
gradeCutoffTemplate
=
_
.
template
(
'<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable>'
+
this
.
gradeCutoffTemplate
=
_
.
template
(
'<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable>'
+
'<%= descriptor %>'
+
'<%= descriptor %>'
+
'</span><span class="range"></span>'
+
'</span><span class="range"></span>'
+
'<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>'
+
'<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>'
+
'</li>'
);
'</li>'
);
// Instrument grading scale
// Instrument grading scale
// convert cutoffs to inversely ordered list
// convert cutoffs to inversely ordered list
var
modelCutoffs
=
this
.
model
.
get
(
'grade_cutoffs'
);
var
modelCutoffs
=
this
.
model
.
get
(
'grade_cutoffs'
);
for
(
var
cutoff
in
modelCutoffs
)
{
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
,
this
.
descendingCutoffs
=
_
.
sortBy
(
this
.
descendingCutoffs
,
function
(
gradeEle
)
{
return
-
gradeEle
[
'cutoff'
];
});
function
(
gradeEle
)
{
return
-
gradeEle
[
'cutoff'
];
});
// Instrument grace period
// Instrument grace period
this
.
$el
.
find
(
'#course-grading-graceperiod'
).
timepicker
();
this
.
$el
.
find
(
'#course-grading-graceperiod'
).
timepicker
();
...
@@ -359,330 +374,330 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
...
@@ -359,330 +374,330 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
// instantiates an editor template for each update in the collection
// 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.
// Because this calls render, put it after everything which render may depend upon to prevent race condition.
window
.
templateLoader
.
loadRemoteTemplate
(
"course_grade_policy"
,
window
.
templateLoader
.
loadRemoteTemplate
(
"course_grade_policy"
,
"/static/client_templates/course_grade_policy.html"
,
"/static/client_templates/course_grade_policy.html"
,
function
(
raw_template
)
{
function
(
raw_template
)
{
self
.
template
=
_
.
template
(
raw_template
);
self
.
template
=
_
.
template
(
raw_template
);
self
.
render
();
self
.
render
();
}
}
);
);
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'remove'
,
this
.
render
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'remove'
,
this
.
render
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'reset'
,
this
.
render
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'reset'
,
this
.
render
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'add'
,
this
.
render
,
this
);
this
.
model
.
get
(
'graders'
).
on
(
'add'
,
this
.
render
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
},
},
render
:
function
()
{
render
:
function
()
{
// prevent bootstrap race condition by event dispatch
// prevent bootstrap race condition by event dispatch
if
(
!
this
.
template
)
return
;
if
(
!
this
.
template
)
return
;
// Create and render the grading type subs
// Create and render the grading type subs
var
self
=
this
;
var
self
=
this
;
var
gradelist
=
this
.
$el
.
find
(
'.course-grading-assignment-list'
);
var
gradelist
=
this
.
$el
.
find
(
'.course-grading-assignment-list'
);
// Undo the double invocation error. At some point, fix the double invocation
// Undo the double invocation error. At some point, fix the double invocation
$
(
gradelist
).
empty
();
$
(
gradelist
).
empty
();
var
gradeCollection
=
this
.
model
.
get
(
'graders'
);
var
gradeCollection
=
this
.
model
.
get
(
'graders'
);
gradeCollection
.
each
(
function
(
gradeModel
)
{
gradeCollection
.
each
(
function
(
gradeModel
)
{
$
(
gradelist
).
append
(
self
.
template
({
model
:
gradeModel
}));
$
(
gradelist
).
append
(
self
.
template
({
model
:
gradeModel
}));
var
newEle
=
gradelist
.
children
().
last
();
var
newEle
=
gradelist
.
children
().
last
();
var
newView
=
new
CMS
.
Views
.
Settings
.
GraderView
({
el
:
newEle
,
var
newView
=
new
CMS
.
Views
.
Settings
.
GraderView
({
el
:
newEle
,
model
:
gradeModel
,
collection
:
gradeCollection
});
model
:
gradeModel
,
collection
:
gradeCollection
});
});
});
// render the grade cutoffs
// render the grade cutoffs
this
.
renderCutoffBar
();
this
.
renderCutoffBar
();
var
graceEle
=
this
.
$el
.
find
(
'#course-grading-graceperiod'
);
var
graceEle
=
this
.
$el
.
find
(
'#course-grading-graceperiod'
);
graceEle
.
timepicker
({
'timeFormat'
:
'H:i'
});
// init doesn't take setTime
graceEle
.
timepicker
({
'timeFormat'
:
'H:i'
});
// init doesn't take setTime
if
(
this
.
model
.
has
(
'grace_period'
))
graceEle
.
timepicker
(
'setTime'
,
this
.
model
.
gracePeriodToDate
());
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
// remove any existing listeners to keep them from piling on b/c render gets called frequently
graceEle
.
off
(
'change'
,
this
.
setGracePeriod
);
graceEle
.
off
(
'change'
,
this
.
setGracePeriod
);
graceEle
.
on
(
'change'
,
this
,
this
.
setGracePeriod
);
graceEle
.
on
(
'change'
,
this
,
this
.
setGracePeriod
);
return
this
;
return
this
;
},
},
addAssignmentType
:
function
(
e
)
{
addAssignmentType
:
function
(
e
)
{
e
.
preventDefault
();
e
.
preventDefault
();
this
.
model
.
get
(
'graders'
).
push
({});
this
.
model
.
get
(
'graders'
).
push
({});
},
},
fieldToSelectorMap
:
{
fieldToSelectorMap
:
{
'grace_period'
:
'course-grading-graceperiod'
'grace_period'
:
'course-grading-graceperiod'
},
},
setGracePeriod
:
function
(
event
)
{
setGracePeriod
:
function
(
event
)
{
event
.
data
.
clearValidationErrors
();
event
.
data
.
clearValidationErrors
();
var
newVal
=
event
.
data
.
model
.
dateToGracePeriod
(
$
(
event
.
currentTarget
).
timepicker
(
'getTime'
));
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
,
if
(
event
.
data
.
model
.
get
(
'grace_period'
)
!=
newVal
)
event
.
data
.
model
.
save
(
'grace_period'
,
newVal
,
{
error
:
CMS
.
ServerError
});
{
error
:
CMS
.
ServerError
});
},
},
updateModel
:
function
(
event
)
{
updateModel
:
function
(
event
)
{
if
(
!
this
.
selectorToField
[
event
.
currentTarget
.
id
])
return
;
if
(
!
this
.
selectorToField
[
event
.
currentTarget
.
id
])
return
;
switch
(
this
.
selectorToField
[
event
.
currentTarget
.
id
])
{
switch
(
this
.
selectorToField
[
event
.
currentTarget
.
id
])
{
case
'grace_period'
:
// handled above
case
'grace_period'
:
// handled above
break
;
break
;
default
:
default
:
this
.
saveIfChanged
(
event
);
this
.
saveIfChanged
(
event
);
break
;
break
;
}
}
},
},
// Grade sliders attributes and methods
// Grade sliders attributes and methods
// Grade bars are li's ordered A -> F with A taking whole width, B overlaying it with its paint, ...
// 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
// 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
// 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"
// 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)
// A does not have a drag bar (cannot change its upper limit)
// Need to insert new bars in right place.
// Need to insert new bars in right place.
GRADES
:
[
'A'
,
'B'
,
'C'
,
'D'
],
// defaults for new grade designators
GRADES
:
[
'A'
,
'B'
,
'C'
,
'D'
],
// defaults for new grade designators
descendingCutoffs
:
[],
// array of { designation : , cutoff : }
descendingCutoffs
:
[],
// array of { designation : , cutoff : }
gradeBarWidth
:
null
,
// cache of value since it won't change (more certain)
gradeBarWidth
:
null
,
// cache of value since it won't change (more certain)
renderCutoffBar
:
function
()
{
renderCutoffBar
:
function
()
{
var
gradeBar
=
this
.
$el
.
find
(
'.grade-bar'
);
var
gradeBar
=
this
.
$el
.
find
(
'.grade-bar'
);
this
.
gradeBarWidth
=
gradeBar
.
width
();
this
.
gradeBarWidth
=
gradeBar
.
width
();
var
gradelist
=
gradeBar
.
children
(
'.grades'
);
var
gradelist
=
gradeBar
.
children
(
'.grades'
);
// HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
// HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
gradelist
.
empty
();
gradelist
.
empty
();
var
nextWidth
=
100
;
// first width is 100%
var
nextWidth
=
100
;
// first width is 100%
// Can probably be simplified to one variable now.
// Can probably be simplified to one variable now.
var
removable
=
false
;
var
removable
=
false
;
var
draggable
=
false
;
// first and last are not removable, first is not draggable
var
draggable
=
false
;
// first and last are not removable, first is not draggable
_
.
each
(
this
.
descendingCutoffs
,
_
.
each
(
this
.
descendingCutoffs
,
function
(
cutoff
,
index
)
{
function
(
cutoff
,
index
)
{
var
newBar
=
this
.
gradeCutoffTemplate
({
var
newBar
=
this
.
gradeCutoffTemplate
({
descriptor
:
cutoff
[
'designation'
]
,
descriptor
:
cutoff
[
'designation'
]
,
width
:
nextWidth
,
width
:
nextWidth
,
removable
:
removable
});
removable
:
removable
});
gradelist
.
append
(
newBar
);
gradelist
.
append
(
newBar
);
if
(
draggable
)
{
if
(
draggable
)
{
newBar
=
gradelist
.
children
().
last
();
// get the dom object not the unparsed string
newBar
=
gradelist
.
children
().
last
();
// get the dom object not the unparsed string
newBar
.
resizable
({
newBar
.
resizable
({
handles
:
"e"
,
handles
:
"e"
,
containment
:
"parent"
,
containment
:
"parent"
,
start
:
this
.
startMoveClosure
(),
start
:
this
.
startMoveClosure
(),
resize
:
this
.
moveBarClosure
(),
resize
:
this
.
moveBarClosure
(),
stop
:
this
.
stopDragClosure
()
stop
:
this
.
stopDragClosure
()
});
});
}
}
// prepare for next
// prepare for next
nextWidth
=
cutoff
[
'cutoff'
];
nextWidth
=
cutoff
[
'cutoff'
];
removable
=
true
;
// first is not removable, all others are
removable
=
true
;
// first is not removable, all others are
draggable
=
true
;
draggable
=
true
;
},
},
this
);
this
);
// add fail which is not in data
// add fail which is not in data
var
failBar
=
this
.
gradeCutoffTemplate
({
descriptor
:
this
.
failLabel
(),
var
failBar
=
this
.
gradeCutoffTemplate
({
descriptor
:
this
.
failLabel
(),
width
:
nextWidth
,
removable
:
false
});
width
:
nextWidth
,
removable
:
false
});
$
(
failBar
).
find
(
"span[contenteditable=true]"
).
attr
(
"contenteditable"
,
false
);
$
(
failBar
).
find
(
"span[contenteditable=true]"
).
attr
(
"contenteditable"
,
false
);
gradelist
.
append
(
failBar
);
gradelist
.
append
(
failBar
);
gradelist
.
children
().
last
().
resizable
({
gradelist
.
children
().
last
().
resizable
({
handles
:
"e"
,
handles
:
"e"
,
containment
:
"parent"
,
containment
:
"parent"
,
start
:
this
.
startMoveClosure
(),
start
:
this
.
startMoveClosure
(),
resize
:
this
.
moveBarClosure
(),
resize
:
this
.
moveBarClosure
(),
stop
:
this
.
stopDragClosure
()
stop
:
this
.
stopDragClosure
()
});
});
this
.
renderGradeRanges
();
this
.
renderGradeRanges
();
},
},
showSettingsExtras
:
function
(
event
)
{
showSettingsExtras
:
function
(
event
)
{
$
(
event
.
currentTarget
).
toggleClass
(
'active'
);
$
(
event
.
currentTarget
).
toggleClass
(
'active'
);
$
(
event
.
currentTarget
).
siblings
.
toggleClass
(
'is-shown'
);
$
(
event
.
currentTarget
).
siblings
.
toggleClass
(
'is-shown'
);
},
},
startMoveClosure
:
function
()
{
startMoveClosure
:
function
()
{
// set min/max widths
// set min/max widths
var
cachethis
=
this
;
var
cachethis
=
this
;
var
widthPerPoint
=
cachethis
.
gradeBarWidth
/
100
;
var
widthPerPoint
=
cachethis
.
gradeBarWidth
/
100
;
return
function
(
event
,
ui
)
{
return
function
(
event
,
ui
)
{
var
barIndex
=
ui
.
element
.
index
();
var
barIndex
=
ui
.
element
.
index
();
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
// 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
);
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
// 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
);
var
max
=
(
barIndex
>=
2
?
cachethis
.
descendingCutoffs
[
barIndex
-
2
][
'cutoff'
]
-
3
:
97
);
ui
.
element
.
resizable
(
"option"
,{
minWidth
:
min
*
widthPerPoint
,
maxWidth
:
max
*
widthPerPoint
});
ui
.
element
.
resizable
(
"option"
,{
minWidth
:
min
*
widthPerPoint
,
maxWidth
:
max
*
widthPerPoint
});
};
};
},
},
moveBarClosure
:
function
()
{
moveBarClosure
:
function
()
{
// 0th ele doesn't have a bar; so, will never invoke this
// 0th ele doesn't have a bar; so, will never invoke this
var
cachethis
=
this
;
var
cachethis
=
this
;
return
function
(
event
,
ui
)
{
return
function
(
event
,
ui
)
{
var
barIndex
=
ui
.
element
.
index
();
var
barIndex
=
ui
.
element
.
index
();
// min and max represent limits not labels (note, can's make smaller than 3 points wide)
// 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
);
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
// 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
max
=
(
barIndex
>=
2
?
cachethis
.
descendingCutoffs
[
barIndex
-
2
][
'cutoff'
]
-
3
:
100
);
var
percentage
=
Math
.
min
(
Math
.
max
(
ui
.
size
.
width
/
cachethis
.
gradeBarWidth
*
100
,
min
),
max
);
var
percentage
=
Math
.
min
(
Math
.
max
(
ui
.
size
.
width
/
cachethis
.
gradeBarWidth
*
100
,
min
),
max
);
cachethis
.
descendingCutoffs
[
barIndex
-
1
][
'cutoff'
]
=
Math
.
round
(
percentage
);
cachethis
.
descendingCutoffs
[
barIndex
-
1
][
'cutoff'
]
=
Math
.
round
(
percentage
);
cachethis
.
renderGradeRanges
();
cachethis
.
renderGradeRanges
();
};
};
},
},
renderGradeRanges
:
function
()
{
renderGradeRanges
:
function
()
{
// the labels showing the range e.g., 71-80
// the labels showing the range e.g., 71-80
var
cutoffs
=
this
.
descendingCutoffs
;
var
cutoffs
=
this
.
descendingCutoffs
;
this
.
$el
.
find
(
'.range'
).
each
(
function
(
i
)
{
this
.
$el
.
find
(
'.range'
).
each
(
function
(
i
)
{
var
min
=
(
i
<
cutoffs
.
length
?
cutoffs
[
i
][
'cutoff'
]
:
0
);
var
min
=
(
i
<
cutoffs
.
length
?
cutoffs
[
i
][
'cutoff'
]
:
0
);
var
max
=
(
i
>
0
?
cutoffs
[
i
-
1
][
'cutoff'
]
:
100
);
var
max
=
(
i
>
0
?
cutoffs
[
i
-
1
][
'cutoff'
]
:
100
);
$
(
this
).
text
(
min
+
'-'
+
max
);
$
(
this
).
text
(
min
+
'-'
+
max
);
});
});
},
},
stopDragClosure
:
function
()
{
stopDragClosure
:
function
()
{
var
cachethis
=
this
;
var
cachethis
=
this
;
return
function
(
event
,
ui
)
{
return
function
(
event
,
ui
)
{
// for some reason the resize is setting height to 0
// for some reason the resize is setting height to 0
cachethis
.
saveCutoffs
();
cachethis
.
saveCutoffs
();
};
};
},
},
saveCutoffs
:
function
()
{
saveCutoffs
:
function
()
{
this
.
model
.
save
(
'grade_cutoffs'
,
this
.
model
.
save
(
'grade_cutoffs'
,
_
.
reduce
(
this
.
descendingCutoffs
,
_
.
reduce
(
this
.
descendingCutoffs
,
function
(
object
,
cutoff
)
{
function
(
object
,
cutoff
)
{
object
[
cutoff
[
'designation'
]]
=
cutoff
[
'cutoff'
]
/
100.0
;
object
[
cutoff
[
'designation'
]]
=
cutoff
[
'cutoff'
]
/
100.0
;
return
object
;
return
object
;
},
},
{}),
{}),
{
error
:
CMS
.
ServerError
});
{
error
:
CMS
.
ServerError
});
},
},
addNewGrade
:
function
(
e
)
{
addNewGrade
:
function
(
e
)
{
e
.
preventDefault
();
e
.
preventDefault
();
var
gradeLength
=
this
.
descendingCutoffs
.
length
;
// cutoffs doesn't include fail/f so this is only the passing grades
var
gradeLength
=
this
.
descendingCutoffs
.
length
;
// cutoffs doesn't include fail/f so this is only the passing grades
if
(
gradeLength
>
3
)
{
if
(
gradeLength
>
3
)
{
// TODO shouldn't we disable the button
// TODO shouldn't we disable the button
return
;
return
;
}
}
var
failBarWidth
=
this
.
descendingCutoffs
[
gradeLength
-
1
][
'cutoff'
];
var
failBarWidth
=
this
.
descendingCutoffs
[
gradeLength
-
1
][
'cutoff'
];
// going to split the grade above the insertion point in half leaving fail in same place
// 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
nextGradeTop
=
(
gradeLength
>
1
?
this
.
descendingCutoffs
[
gradeLength
-
2
][
'cutoff'
]
:
100
);
var
targetWidth
=
failBarWidth
+
((
nextGradeTop
-
failBarWidth
)
/
2
);
var
targetWidth
=
failBarWidth
+
((
nextGradeTop
-
failBarWidth
)
/
2
);
this
.
descendingCutoffs
.
push
({
designation
:
this
.
GRADES
[
gradeLength
],
cutoff
:
failBarWidth
});
this
.
descendingCutoffs
.
push
({
designation
:
this
.
GRADES
[
gradeLength
],
cutoff
:
failBarWidth
});
this
.
descendingCutoffs
[
gradeLength
-
1
][
'cutoff'
]
=
Math
.
round
(
targetWidth
);
this
.
descendingCutoffs
[
gradeLength
-
1
][
'cutoff'
]
=
Math
.
round
(
targetWidth
);
var
$newGradeBar
=
this
.
gradeCutoffTemplate
({
descriptor
:
this
.
GRADES
[
gradeLength
],
var
$newGradeBar
=
this
.
gradeCutoffTemplate
({
descriptor
:
this
.
GRADES
[
gradeLength
],
width
:
targetWidth
,
removable
:
true
});
width
:
targetWidth
,
removable
:
true
});
var
gradeDom
=
this
.
$el
.
find
(
'.grades'
);
var
gradeDom
=
this
.
$el
.
find
(
'.grades'
);
gradeDom
.
children
().
last
().
before
(
$newGradeBar
);
gradeDom
.
children
().
last
().
before
(
$newGradeBar
);
var
newEle
=
gradeDom
.
children
()[
gradeLength
];
var
newEle
=
gradeDom
.
children
()[
gradeLength
];
$
(
newEle
).
resizable
({
$
(
newEle
).
resizable
({
handles
:
"e"
,
handles
:
"e"
,
containment
:
"parent"
,
containment
:
"parent"
,
start
:
this
.
startMoveClosure
(),
start
:
this
.
startMoveClosure
(),
resize
:
this
.
moveBarClosure
(),
resize
:
this
.
moveBarClosure
(),
stop
:
this
.
stopDragClosure
()
stop
:
this
.
stopDragClosure
()
});
});
// Munge existing grade labels?
// Munge existing grade labels?
// If going from Pass/Fail to 3 levels, change to Pass to A
// If going from Pass/Fail to 3 levels, change to Pass to A
if
(
gradeLength
===
1
&&
this
.
descendingCutoffs
[
0
][
'designation'
]
===
'Pass'
)
{
if
(
gradeLength
===
1
&&
this
.
descendingCutoffs
[
0
][
'designation'
]
===
'Pass'
)
{
this
.
descendingCutoffs
[
0
][
'designation'
]
=
this
.
GRADES
[
0
];
this
.
descendingCutoffs
[
0
][
'designation'
]
=
this
.
GRADES
[
0
];
this
.
setTopGradeLabel
();
this
.
setTopGradeLabel
();
}
}
this
.
setFailLabel
();
this
.
setFailLabel
();
this
.
renderGradeRanges
();
this
.
renderGradeRanges
();
this
.
saveCutoffs
();
this
.
saveCutoffs
();
},
},
removeGrade
:
function
(
e
)
{
removeGrade
:
function
(
e
)
{
e
.
preventDefault
();
e
.
preventDefault
();
var
domElement
=
$
(
e
.
currentTarget
).
closest
(
'li'
);
var
domElement
=
$
(
e
.
currentTarget
).
closest
(
'li'
);
var
index
=
domElement
.
index
();
var
index
=
domElement
.
index
();
// copy the boundary up to the next higher grade then remove
// copy the boundary up to the next higher grade then remove
this
.
descendingCutoffs
[
index
-
1
][
'cutoff'
]
=
this
.
descendingCutoffs
[
index
][
'cutoff'
];
this
.
descendingCutoffs
[
index
-
1
][
'cutoff'
]
=
this
.
descendingCutoffs
[
index
][
'cutoff'
];
this
.
descendingCutoffs
.
splice
(
index
,
1
);
this
.
descendingCutoffs
.
splice
(
index
,
1
);
domElement
.
remove
();
domElement
.
remove
();
if
(
this
.
descendingCutoffs
.
length
===
1
&&
this
.
descendingCutoffs
[
0
][
'designation'
]
===
this
.
GRADES
[
0
])
{
if
(
this
.
descendingCutoffs
.
length
===
1
&&
this
.
descendingCutoffs
[
0
][
'designation'
]
===
this
.
GRADES
[
0
])
{
this
.
descendingCutoffs
[
0
][
'designation'
]
=
'Pass'
;
this
.
descendingCutoffs
[
0
][
'designation'
]
=
'Pass'
;
this
.
setTopGradeLabel
();
this
.
setTopGradeLabel
();
}
}
this
.
setFailLabel
();
this
.
setFailLabel
();
this
.
renderGradeRanges
();
this
.
renderGradeRanges
();
this
.
saveCutoffs
();
this
.
saveCutoffs
();
},
},
updateDesignation
:
function
(
e
)
{
updateDesignation
:
function
(
e
)
{
var
index
=
$
(
e
.
currentTarget
).
closest
(
'li'
).
index
();
var
index
=
$
(
e
.
currentTarget
).
closest
(
'li'
).
index
();
this
.
descendingCutoffs
[
index
][
'designation'
]
=
$
(
e
.
currentTarget
).
html
();
this
.
descendingCutoffs
[
index
][
'designation'
]
=
$
(
e
.
currentTarget
).
html
();
this
.
saveCutoffs
();
this
.
saveCutoffs
();
},
},
failLabel
:
function
()
{
failLabel
:
function
()
{
if
(
this
.
descendingCutoffs
.
length
===
1
)
return
'Fail'
;
if
(
this
.
descendingCutoffs
.
length
===
1
)
return
'Fail'
;
else
return
'F'
;
else
return
'F'
;
},
},
setFailLabel
:
function
()
{
setFailLabel
:
function
()
{
this
.
$el
.
find
(
'.grades .letter-grade'
).
last
().
html
(
this
.
failLabel
());
this
.
$el
.
find
(
'.grades .letter-grade'
).
last
().
html
(
this
.
failLabel
());
},
},
setTopGradeLabel
:
function
()
{
setTopGradeLabel
:
function
()
{
this
.
$el
.
find
(
'.grades .letter-grade'
).
first
().
html
(
this
.
descendingCutoffs
[
0
][
'designation'
]);
this
.
$el
.
find
(
'.grades .letter-grade'
).
first
().
html
(
this
.
descendingCutoffs
[
0
][
'designation'
]);
}
}
});
});
CMS
.
Views
.
Settings
.
GraderView
=
CMS
.
Views
.
ValidatingView
.
extend
({
CMS
.
Views
.
Settings
.
GraderView
=
CMS
.
Views
.
ValidatingView
.
extend
({
// Model class is CMS.Models.Settings.CourseGrader
// Model class is CMS.Models.Settings.CourseGrader
events
:
{
events
:
{
"blur
input"
:
"updateModel"
,
"change
input"
:
"updateModel"
,
"blur
textarea"
:
"updateModel"
,
"change
textarea"
:
"updateModel"
,
"click .remove-grading-data"
:
"deleteModel"
"click .remove-grading-data"
:
"deleteModel"
},
},
initialize
:
function
()
{
initialize
:
function
()
{
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
model
.
on
(
'error'
,
this
.
handleValidationError
,
this
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
this
.
selectorToField
=
_
.
invert
(
this
.
fieldToSelectorMap
);
this
.
render
();
this
.
render
();
},
},
render
:
function
()
{
render
:
function
()
{
return
this
;
return
this
;
},
},
fieldToSelectorMap
:
{
fieldToSelectorMap
:
{
'type'
:
'course-grading-assignment-name'
,
'type'
:
'course-grading-assignment-name'
,
'short_label'
:
'course-grading-assignment-shortname'
,
'short_label'
:
'course-grading-assignment-shortname'
,
'min_count'
:
'course-grading-assignment-totalassignments'
,
'min_count'
:
'course-grading-assignment-totalassignments'
,
'drop_count'
:
'course-grading-assignment-droppable'
,
'drop_count'
:
'course-grading-assignment-droppable'
,
'weight'
:
'course-grading-assignment-gradeweight'
'weight'
:
'course-grading-assignment-gradeweight'
},
},
updateModel
:
function
(
event
)
{
updateModel
:
function
(
event
)
{
// HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving
// 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
// 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.]
// give 2 assignments the same name.]
if
(
!
this
.
model
.
collection
)
{
if
(
!
this
.
model
.
collection
)
{
this
.
model
.
collection
=
this
.
collection
;
this
.
model
.
collection
=
this
.
collection
;
}
}
switch
(
event
.
currentTarget
.
id
)
{
switch
(
event
.
currentTarget
.
id
)
{
case
'course-grading-assignment-totalassignments'
:
case
'course-grading-assignment-totalassignments'
:
this
.
$el
.
find
(
'#course-grading-assignment-droppable'
).
attr
(
'max'
,
$
(
event
.
currentTarget
).
val
());
this
.
$el
.
find
(
'#course-grading-assignment-droppable'
).
attr
(
'max'
,
$
(
event
.
currentTarget
).
val
());
this
.
saveIfChanged
(
event
);
this
.
saveIfChanged
(
event
);
break
;
break
;
case
'course-grading-assignment-name'
:
case
'course-grading-assignment-name'
:
var
oldName
=
this
.
model
.
get
(
'type'
);
var
oldName
=
this
.
model
.
get
(
'type'
);
if
(
this
.
saveIfChanged
(
event
)
&&
!
_
.
isEmpty
(
oldName
))
{
if
(
this
.
saveIfChanged
(
event
)
&&
!
_
.
isEmpty
(
oldName
))
{
// overload the error display logic
// overload the error display logic
this
.
_cacheValidationErrors
.
push
(
event
.
currentTarget
);
this
.
_cacheValidationErrors
.
push
(
event
.
currentTarget
);
$
(
event
.
currentTarget
).
parent
().
append
(
$
(
event
.
currentTarget
).
parent
().
append
(
this
.
errorTemplate
({
message
:
'For grading to work, you must change all "'
+
oldName
+
this
.
errorTemplate
({
message
:
'For grading to work, you must change all "'
+
oldName
+
'" subsections to "'
+
this
.
model
.
get
(
'type'
)
+
'".'
}));
'" subsections to "'
+
this
.
model
.
get
(
'type'
)
+
'".'
}));
}
}
break
;
break
;
default
:
default
:
this
.
saveIfChanged
(
event
);
this
.
saveIfChanged
(
event
);
break
;
break
;
}
}
},
},
deleteModel
:
function
(
e
)
{
deleteModel
:
function
(
e
)
{
this
.
model
.
destroy
(
this
.
model
.
destroy
(
{
error
:
CMS
.
ServerError
});
{
error
:
CMS
.
ServerError
});
e
.
preventDefault
();
e
.
preventDefault
();
}
}
});
});
\ No newline at end of file
cms/templates/settings.html
View file @
8f16d639
<
%
inherit
file=
"base.html"
/>
<
%
inherit
file=
"base.html"
/>
<
%!
from
django
.
core
.
urlresolvers
import
reverse
%
>
<
%
block
name=
"bodyclass"
>
settings
</
%
block>
<
%
block
name=
"bodyclass"
>
settings
</
%
block>
<
%
block
name=
"title"
>
Settings
</
%
block>
<
%
block
name=
"title"
>
Settings
</
%
block>
...
@@ -15,24 +16,28 @@ from contentstore import utils
...
@@ -15,24 +16,28 @@ from contentstore import utils
<script
src=
"${static.url('js/vendor/date.js')}"
></script>
<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/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/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_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_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/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/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"
>
<script
type=
"text/javascript"
>
$
(
document
).
ready
(
function
(){
$
(
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
({
var
settingsModel
=
new
CMS
.
Models
.
Settings
.
CourseSettings
({
courseLocation
:
new
CMS
.
Models
.
Location
(
'${context_course.location}'
,{
parse
:
true
}),
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
({
var
editor
=
new
CMS
.
Views
.
Settings
.
Main
({
el
:
$
(
'.main-wrapper'
),
el
:
$
(
'.main-wrapper'
),
model
:
settingsModel
model
:
settingsModel
...
@@ -743,90 +748,6 @@ from contentstore import utils
...
@@ -743,90 +748,6 @@ from contentstore import utils
<!-- basic empty & initial empty field (if user had no values yet) -->
<!-- basic empty & initial empty field (if user had no values yet) -->
<ul
class=
"input-list course-advanced-policy-list"
>
<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>
</ul>
<!-- advanced policy actions -->
<!-- advanced policy actions -->
...
@@ -838,14 +759,6 @@ from contentstore import utils
...
@@ -838,14 +759,6 @@ from contentstore import utils
</a>
</a>
</div>
</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>
</div>
</div>
</section>
<!-- .settings-advanced-policies -->
</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