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
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
358 additions
and
229 deletions
+358
-229
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
+0
-0
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
This diff is collapsed.
Click to expand it.
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
This diff is collapsed.
Click to expand it.
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