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
319eb0ba
Commit
319eb0ba
authored
Jun 17, 2013
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #170 from edx/feature/cdodge/soft-delete-assets
Feature/cdodge/soft delete assets
parents
67bcbe22
0d6f2139
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
487 additions
and
87 deletions
+487
-87
CHANGELOG.rst
+2
-0
cms/djangoapps/contentstore/management/commands/empty_asset_trashcan.py
+25
-0
cms/djangoapps/contentstore/management/commands/restore_asset_from_trashcan.py
+13
-0
cms/djangoapps/contentstore/tests/test_contentstore.py
+128
-0
cms/djangoapps/contentstore/views/assets.py
+61
-1
cms/envs/common.py
+2
-1
cms/envs/dev.py
+6
-1
cms/envs/test.py
+7
-1
cms/static/js/base.js
+0
-71
cms/static/js/models/feedback.js
+6
-0
cms/static/js/views/assets.js
+129
-0
cms/static/sass/views/_assets.scss
+4
-0
cms/templates/asset_index.html
+33
-1
cms/urls.py
+5
-0
common/lib/xmodule/xmodule/contentstore/django.py
+8
-5
common/lib/xmodule/xmodule/contentstore/mongo.py
+9
-6
common/lib/xmodule/xmodule/contentstore/utils.py
+49
-0
No files found.
CHANGELOG.rst
View file @
319eb0ba
...
@@ -15,6 +15,8 @@ Blades: Video Alpha bug fix for speed changing to 1.0 in Firefox.
...
@@ -15,6 +15,8 @@ Blades: Video Alpha bug fix for speed changing to 1.0 in Firefox.
Blades: Additional event tracking added to Video Alpha: fullscreen switch, show/hide
Blades: Additional event tracking added to Video Alpha: fullscreen switch, show/hide
captions.
captions.
CMS: Allow editors to delete uploaded files/assets
LMS: Some errors handling Non-ASCII data in XML courses have been fixed.
LMS: Some errors handling Non-ASCII data in XML courses have been fixed.
LMS: Add page-load tracking using segment-io (if SEGMENT_IO_LMS_KEY and
LMS: Add page-load tracking using segment-io (if SEGMENT_IO_LMS_KEY and
...
...
cms/djangoapps/contentstore/management/commands/empty_asset_trashcan.py
0 → 100644
View file @
319eb0ba
from
django.core.management.base
import
BaseCommand
,
CommandError
from
xmodule.course_module
import
CourseDescriptor
from
xmodule.contentstore.utils
import
empty_asset_trashcan
from
xmodule.modulestore.django
import
modulestore
from
.prompt
import
query_yes_no
class
Command
(
BaseCommand
):
help
=
'''Empty the trashcan. Can pass an optional course_id to limit the damage.'''
def
handle
(
self
,
*
args
,
**
options
):
if
len
(
args
)
!=
1
and
len
(
args
)
!=
0
:
raise
CommandError
(
"empty_asset_trashcan requires one or no arguments: |<location>|"
)
locs
=
[]
if
len
(
args
)
==
1
:
locs
.
append
(
CourseDescriptor
.
id_to_location
(
args
[
0
]))
else
:
courses
=
modulestore
(
'direct'
)
.
get_courses
()
for
course
in
courses
:
locs
.
append
(
course
.
location
)
if
query_yes_no
(
"Emptying trashcan. Confirm?"
,
default
=
"no"
):
empty_asset_trashcan
(
locs
)
cms/djangoapps/contentstore/management/commands/restore_asset_from_trashcan.py
0 → 100644
View file @
319eb0ba
from
django.core.management.base
import
BaseCommand
,
CommandError
from
xmodule.contentstore.utils
import
restore_asset_from_trashcan
class
Command
(
BaseCommand
):
help
=
'''Restore a deleted asset from the trashcan back to it's original course'''
def
handle
(
self
,
*
args
,
**
options
):
if
len
(
args
)
!=
1
and
len
(
args
)
!=
0
:
raise
CommandError
(
"restore_asset_from_trashcan requires one argument: <location>"
)
restore_asset_from_trashcan
(
args
[
0
])
cms/djangoapps/contentstore/tests/test_contentstore.py
View file @
319eb0ba
...
@@ -28,6 +28,8 @@ from xmodule.templates import update_templates
...
@@ -28,6 +28,8 @@ from xmodule.templates import update_templates
from
xmodule.modulestore.xml_exporter
import
export_to_xml
from
xmodule.modulestore.xml_exporter
import
export_to_xml
from
xmodule.modulestore.xml_importer
import
import_from_xml
,
perform_xlint
from
xmodule.modulestore.xml_importer
import
import_from_xml
,
perform_xlint
from
xmodule.modulestore.inheritance
import
own_metadata
from
xmodule.modulestore.inheritance
import
own_metadata
from
xmodule.contentstore.content
import
StaticContent
from
xmodule.contentstore.utils
import
restore_asset_from_trashcan
,
empty_asset_trashcan
from
xmodule.capa_module
import
CapaDescriptor
from
xmodule.capa_module
import
CapaDescriptor
from
xmodule.course_module
import
CourseDescriptor
from
xmodule.course_module
import
CourseDescriptor
...
@@ -35,6 +37,7 @@ from xmodule.seq_module import SequenceDescriptor
...
@@ -35,6 +37,7 @@ from xmodule.seq_module import SequenceDescriptor
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
contentstore.views.component
import
ADVANCED_COMPONENT_TYPES
from
contentstore.views.component
import
ADVANCED_COMPONENT_TYPES
from
xmodule.exceptions
import
NotFoundError
from
django_comment_common.utils
import
are_permissions_roles_seeded
from
django_comment_common.utils
import
are_permissions_roles_seeded
from
xmodule.exceptions
import
InvalidVersionError
from
xmodule.exceptions
import
InvalidVersionError
...
@@ -382,6 +385,131 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
...
@@ -382,6 +385,131 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
course
=
module_store
.
get_item
(
source_location
)
course
=
module_store
.
get_item
(
source_location
)
self
.
assertFalse
(
course
.
hide_progress_tab
)
self
.
assertFalse
(
course
.
hide_progress_tab
)
def
test_asset_import
(
self
):
'''
This test validates that an image asset is imported and a thumbnail was generated for a .gif
'''
content_store
=
contentstore
()
module_store
=
modulestore
(
'direct'
)
import_from_xml
(
module_store
,
'common/test/data/'
,
[
'full'
],
static_content_store
=
content_store
)
course_location
=
CourseDescriptor
.
id_to_location
(
'edX/full/6.002_Spring_2012'
)
course
=
module_store
.
get_item
(
course_location
)
self
.
assertIsNotNone
(
course
)
# make sure we have some assets in our contentstore
all_assets
=
content_store
.
get_all_content_for_course
(
course_location
)
self
.
assertGreater
(
len
(
all_assets
),
0
)
# make sure we have some thumbnails in our contentstore
all_thumbnails
=
content_store
.
get_all_content_thumbnails_for_course
(
course_location
)
self
.
assertGreater
(
len
(
all_thumbnails
),
0
)
content
=
None
try
:
location
=
StaticContent
.
get_location_from_path
(
'/c4x/edX/full/asset/circuits_duality.gif'
)
content
=
content_store
.
find
(
location
)
except
NotFoundError
:
pass
self
.
assertIsNotNone
(
content
)
self
.
assertIsNotNone
(
content
.
thumbnail_location
)
thumbnail
=
None
try
:
thumbnail
=
content_store
.
find
(
content
.
thumbnail_location
)
except
:
pass
self
.
assertIsNotNone
(
thumbnail
)
def
test_asset_delete_and_restore
(
self
):
'''
This test will exercise the soft delete/restore functionality of the assets
'''
content_store
=
contentstore
()
trash_store
=
contentstore
(
'trashcan'
)
module_store
=
modulestore
(
'direct'
)
import_from_xml
(
module_store
,
'common/test/data/'
,
[
'full'
],
static_content_store
=
content_store
)
# look up original (and thumbnail) in content store, should be there after import
location
=
StaticContent
.
get_location_from_path
(
'/c4x/edX/full/asset/circuits_duality.gif'
)
content
=
content_store
.
find
(
location
,
throw_on_not_found
=
False
)
thumbnail_location
=
content
.
thumbnail_location
self
.
assertIsNotNone
(
content
)
self
.
assertIsNotNone
(
thumbnail_location
)
# go through the website to do the delete, since the soft-delete logic is in the view
url
=
reverse
(
'remove_asset'
,
kwargs
=
{
'org'
:
'edX'
,
'course'
:
'full'
,
'name'
:
'6.002_Spring_2012'
})
resp
=
self
.
client
.
post
(
url
,
{
'location'
:
'/c4x/edX/full/asset/circuits_duality.gif'
})
self
.
assertEqual
(
resp
.
status_code
,
200
)
asset_location
=
StaticContent
.
get_location_from_path
(
'/c4x/edX/full/asset/circuits_duality.gif'
)
# now try to find it in store, but they should not be there any longer
content
=
content_store
.
find
(
asset_location
,
throw_on_not_found
=
False
)
thumbnail
=
content_store
.
find
(
thumbnail_location
,
throw_on_not_found
=
False
)
self
.
assertIsNone
(
content
)
self
.
assertIsNone
(
thumbnail
)
# now try to find it and the thumbnail in trashcan - should be in there
content
=
trash_store
.
find
(
asset_location
,
throw_on_not_found
=
False
)
thumbnail
=
trash_store
.
find
(
thumbnail_location
,
throw_on_not_found
=
False
)
self
.
assertIsNotNone
(
content
)
self
.
assertIsNotNone
(
thumbnail
)
# let's restore the asset
restore_asset_from_trashcan
(
'/c4x/edX/full/asset/circuits_duality.gif'
)
# now try to find it in courseware store, and they should be back after restore
content
=
content_store
.
find
(
asset_location
,
throw_on_not_found
=
False
)
thumbnail
=
content_store
.
find
(
thumbnail_location
,
throw_on_not_found
=
False
)
self
.
assertIsNotNone
(
content
)
self
.
assertIsNotNone
(
thumbnail
)
def
test_empty_trashcan
(
self
):
'''
This test will exercise the empting of the asset trashcan
'''
content_store
=
contentstore
()
trash_store
=
contentstore
(
'trashcan'
)
module_store
=
modulestore
(
'direct'
)
import_from_xml
(
module_store
,
'common/test/data/'
,
[
'full'
],
static_content_store
=
content_store
)
course_location
=
CourseDescriptor
.
id_to_location
(
'edX/full/6.002_Spring_2012'
)
location
=
StaticContent
.
get_location_from_path
(
'/c4x/edX/full/asset/circuits_duality.gif'
)
content
=
content_store
.
find
(
location
,
throw_on_not_found
=
False
)
self
.
assertIsNotNone
(
content
)
# go through the website to do the delete, since the soft-delete logic is in the view
url
=
reverse
(
'remove_asset'
,
kwargs
=
{
'org'
:
'edX'
,
'course'
:
'full'
,
'name'
:
'6.002_Spring_2012'
})
resp
=
self
.
client
.
post
(
url
,
{
'location'
:
'/c4x/edX/full/asset/circuits_duality.gif'
})
self
.
assertEqual
(
resp
.
status_code
,
200
)
# make sure there's something in the trashcan
all_assets
=
trash_store
.
get_all_content_for_course
(
course_location
)
self
.
assertGreater
(
len
(
all_assets
),
0
)
# make sure we have some thumbnails in our trashcan
all_thumbnails
=
trash_store
.
get_all_content_thumbnails_for_course
(
course_location
)
self
.
assertGreater
(
len
(
all_thumbnails
),
0
)
# empty the trashcan
empty_asset_trashcan
([
course_location
])
# make sure trashcan is empty
all_assets
=
trash_store
.
get_all_content_for_course
(
course_location
)
all_thumbnails
=
trash_store
.
get_all_content_thumbnails_for_course
(
course_location
)
self
.
assertEqual
(
len
(
all_assets
),
0
)
self
.
assertEqual
(
len
(
all_thumbnails
),
0
)
def
test_clone_course
(
self
):
def
test_clone_course
(
self
):
course_data
=
{
course_data
=
{
...
...
cms/djangoapps/contentstore/views/assets.py
View file @
319eb0ba
...
@@ -25,6 +25,8 @@ from xmodule.modulestore.django import modulestore
...
@@ -25,6 +25,8 @@ from xmodule.modulestore.django import modulestore
from
xmodule.modulestore
import
Location
from
xmodule.modulestore
import
Location
from
xmodule.contentstore.content
import
StaticContent
from
xmodule.contentstore.content
import
StaticContent
from
xmodule.util.date_utils
import
get_default_time_display
from
xmodule.util.date_utils
import
get_default_time_display
from
xmodule.modulestore
import
InvalidLocationError
from
xmodule.exceptions
import
NotFoundError
from
..utils
import
get_url_reverse
from
..utils
import
get_url_reverse
from
.access
import
get_location_and_verify_access
from
.access
import
get_location_and_verify_access
...
@@ -78,10 +80,17 @@ def asset_index(request, org, course, name):
...
@@ -78,10 +80,17 @@ def asset_index(request, org, course, name):
'active_tab'
:
'assets'
,
'active_tab'
:
'assets'
,
'context_course'
:
course_module
,
'context_course'
:
course_module
,
'assets'
:
asset_display
,
'assets'
:
asset_display
,
'upload_asset_callback_url'
:
upload_asset_callback_url
'upload_asset_callback_url'
:
upload_asset_callback_url
,
'remove_asset_callback_url'
:
reverse
(
'remove_asset'
,
kwargs
=
{
'org'
:
org
,
'course'
:
course
,
'name'
:
name
})
})
})
@login_required
@ensure_csrf_cookie
def
upload_asset
(
request
,
org
,
course
,
coursename
):
def
upload_asset
(
request
,
org
,
course
,
coursename
):
'''
'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
cdodge: this method allows for POST uploading of files into the course asset library, which will
...
@@ -147,6 +156,57 @@ def upload_asset(request, org, course, coursename):
...
@@ -147,6 +156,57 @@ def upload_asset(request, org, course, coursename):
@ensure_csrf_cookie
@ensure_csrf_cookie
@login_required
@login_required
def
remove_asset
(
request
,
org
,
course
,
name
):
'''
This method will perform a 'soft-delete' of an asset, which is basically to copy the asset from
the main GridFS collection and into a Trashcan
'''
get_location_and_verify_access
(
request
,
org
,
course
,
name
)
location
=
request
.
POST
[
'location'
]
# make sure the location is valid
try
:
loc
=
StaticContent
.
get_location_from_path
(
location
)
except
InvalidLocationError
:
# return a 'Bad Request' to browser as we have a malformed Location
response
=
HttpResponse
()
response
.
status_code
=
400
return
response
# also make sure the item to delete actually exists
try
:
content
=
contentstore
()
.
find
(
loc
)
except
NotFoundError
:
response
=
HttpResponse
()
response
.
status_code
=
404
return
response
# ok, save the content into the trashcan
contentstore
(
'trashcan'
)
.
save
(
content
)
# see if there is a thumbnail as well, if so move that as well
if
content
.
thumbnail_location
is
not
None
:
try
:
thumbnail_content
=
contentstore
()
.
find
(
content
.
thumbnail_location
)
contentstore
(
'trashcan'
)
.
save
(
thumbnail_content
)
# hard delete thumbnail from origin
contentstore
()
.
delete
(
thumbnail_content
.
get_id
())
# remove from any caching
del_cached_content
(
thumbnail_content
.
location
)
except
:
pass
# OK if this is left dangling
# delete the original
contentstore
()
.
delete
(
content
.
get_id
())
# remove from cache
del_cached_content
(
content
.
location
)
return
HttpResponse
()
@ensure_csrf_cookie
@login_required
def
import_course
(
request
,
org
,
course
,
name
):
def
import_course
(
request
,
org
,
course
,
name
):
location
=
get_location_and_verify_access
(
request
,
org
,
course
,
name
)
location
=
get_location_and_verify_access
(
request
,
org
,
course
,
name
)
...
...
cms/envs/common.py
View file @
319eb0ba
...
@@ -228,7 +228,8 @@ PIPELINE_JS = {
...
@@ -228,7 +228,8 @@ PIPELINE_JS = {
)
+
[
'js/hesitate.js'
,
'js/base.js'
,
)
+
[
'js/hesitate.js'
,
'js/base.js'
,
'js/models/feedback.js'
,
'js/views/feedback.js'
,
'js/models/feedback.js'
,
'js/views/feedback.js'
,
'js/models/section.js'
,
'js/views/section.js'
,
'js/models/section.js'
,
'js/views/section.js'
,
'js/models/metadata_model.js'
,
'js/views/metadata_editor_view.js'
],
'js/models/metadata_model.js'
,
'js/views/metadata_editor_view.js'
,
'js/views/assets.js'
],
'output_filename'
:
'js/cms-application.js'
,
'output_filename'
:
'js/cms-application.js'
,
'test_order'
:
0
'test_order'
:
0
},
},
...
...
cms/envs/dev.py
View file @
319eb0ba
...
@@ -43,10 +43,15 @@ CONTENTSTORE = {
...
@@ -43,10 +43,15 @@ CONTENTSTORE = {
'OPTIONS'
:
{
'OPTIONS'
:
{
'host'
:
'localhost'
,
'host'
:
'localhost'
,
'db'
:
'xcontent'
,
'db'
:
'xcontent'
,
},
# allow for additional options that can be keyed on a name, e.g. 'trashcan'
'ADDITIONAL_OPTIONS'
:
{
'trashcan'
:
{
'bucket'
:
'trash_fs'
}
}
}
}
}
DATABASES
=
{
DATABASES
=
{
'default'
:
{
'default'
:
{
'ENGINE'
:
'django.db.backends.sqlite3'
,
'ENGINE'
:
'django.db.backends.sqlite3'
,
...
...
cms/envs/test.py
View file @
319eb0ba
...
@@ -70,7 +70,13 @@ CONTENTSTORE = {
...
@@ -70,7 +70,13 @@ CONTENTSTORE = {
'ENGINE'
:
'xmodule.contentstore.mongo.MongoContentStore'
,
'ENGINE'
:
'xmodule.contentstore.mongo.MongoContentStore'
,
'OPTIONS'
:
{
'OPTIONS'
:
{
'host'
:
'localhost'
,
'host'
:
'localhost'
,
'db'
:
'xcontent'
,
'db'
:
'test_xmodule'
,
},
# allow for additional options that can be keyed on a name, e.g. 'trashcan'
'ADDITIONAL_OPTIONS'
:
{
'trashcan'
:
{
'bucket'
:
'trash_fs'
}
}
}
}
}
...
...
cms/static/js/base.js
View file @
319eb0ba
...
@@ -32,8 +32,6 @@ $(document).ready(function() {
...
@@ -32,8 +32,6 @@ $(document).ready(function() {
$modal
.
bind
(
'click'
,
hideModal
);
$modal
.
bind
(
'click'
,
hideModal
);
$modalCover
.
bind
(
'click'
,
hideModal
);
$modalCover
.
bind
(
'click'
,
hideModal
);
$
(
'.uploads .upload-button'
).
bind
(
'click'
,
showUploadModal
);
$
(
'.upload-modal .close-button'
).
bind
(
'click'
,
hideModal
);
$body
.
on
(
'click'
,
'.embeddable-xml-input'
,
function
()
{
$body
.
on
(
'click'
,
'.embeddable-xml-input'
,
function
()
{
$
(
this
).
select
();
$
(
this
).
select
();
...
@@ -145,8 +143,6 @@ $(document).ready(function() {
...
@@ -145,8 +143,6 @@ $(document).ready(function() {
$
(
'.edit-section-start-cancel'
).
bind
(
'click'
,
cancelSetSectionScheduleDate
);
$
(
'.edit-section-start-cancel'
).
bind
(
'click'
,
cancelSetSectionScheduleDate
);
$
(
'.edit-section-start-save'
).
bind
(
'click'
,
saveSetSectionScheduleDate
);
$
(
'.edit-section-start-save'
).
bind
(
'click'
,
saveSetSectionScheduleDate
);
$
(
'.upload-modal .choose-file-button'
).
bind
(
'click'
,
showFileSelectionMenu
);
$body
.
on
(
'click'
,
'.section-published-date .edit-button'
,
editSectionPublishDate
);
$body
.
on
(
'click'
,
'.section-published-date .edit-button'
,
editSectionPublishDate
);
$body
.
on
(
'click'
,
'.section-published-date .schedule-button'
,
editSectionPublishDate
);
$body
.
on
(
'click'
,
'.section-published-date .schedule-button'
,
editSectionPublishDate
);
$body
.
on
(
'click'
,
'.edit-subsection-publish-settings .save-button'
,
saveSetSectionScheduleDate
);
$body
.
on
(
'click'
,
'.edit-subsection-publish-settings .save-button'
,
saveSetSectionScheduleDate
);
...
@@ -398,73 +394,6 @@ function _deleteItem($el) {
...
@@ -398,73 +394,6 @@ function _deleteItem($el) {
});
});
}
}
function
showUploadModal
(
e
)
{
e
.
preventDefault
();
$modal
=
$
(
'.upload-modal'
).
show
();
$
(
'.file-input'
).
bind
(
'change'
,
startUpload
);
$modalCover
.
show
();
}
function
showFileSelectionMenu
(
e
)
{
e
.
preventDefault
();
$
(
'.file-input'
).
click
();
}
function
startUpload
(
e
)
{
var
files
=
$
(
'.file-input'
).
get
(
0
).
files
;
if
(
files
.
length
===
0
)
return
;
$
(
'.upload-modal h1'
).
html
(
gettext
(
'Uploading…'
));
$
(
'.upload-modal .file-name'
).
html
(
files
[
0
].
name
);
$
(
'.upload-modal .file-chooser'
).
ajaxSubmit
({
beforeSend
:
resetUploadBar
,
uploadProgress
:
showUploadFeedback
,
complete
:
displayFinishedUpload
});
$
(
'.upload-modal .choose-file-button'
).
hide
();
$
(
'.upload-modal .progress-bar'
).
removeClass
(
'loaded'
).
show
();
}
function
resetUploadBar
()
{
var
percentVal
=
'0%'
;
$
(
'.upload-modal .progress-fill'
).
width
(
percentVal
);
$
(
'.upload-modal .progress-fill'
).
html
(
percentVal
);
}
function
showUploadFeedback
(
event
,
position
,
total
,
percentComplete
)
{
var
percentVal
=
percentComplete
+
'%'
;
$
(
'.upload-modal .progress-fill'
).
width
(
percentVal
);
$
(
'.upload-modal .progress-fill'
).
html
(
percentVal
);
}
function
displayFinishedUpload
(
xhr
)
{
if
(
xhr
.
status
=
200
)
{
markAsLoaded
();
}
var
resp
=
JSON
.
parse
(
xhr
.
responseText
);
$
(
'.upload-modal .embeddable-xml-input'
).
val
(
xhr
.
getResponseHeader
(
'asset_url'
));
$
(
'.upload-modal .embeddable'
).
show
();
$
(
'.upload-modal .file-name'
).
hide
();
$
(
'.upload-modal .progress-fill'
).
html
(
resp
.
msg
);
$
(
'.upload-modal .choose-file-button'
).
html
(
gettext
(
'Load Another File'
)).
show
();
$
(
'.upload-modal .progress-fill'
).
width
(
'100%'
);
// see if this id already exists, if so, then user must have updated an existing piece of content
$
(
"tr[data-id='"
+
resp
.
url
+
"']"
).
remove
();
var
template
=
$
(
'#new-asset-element'
).
html
();
var
html
=
Mustache
.
to_html
(
template
,
resp
);
$
(
'table > tbody'
).
prepend
(
html
);
analytics
.
track
(
'Uploaded a File'
,
{
'course'
:
course_location_analytics
,
'asset_url'
:
resp
.
url
});
}
function
markAsLoaded
()
{
function
markAsLoaded
()
{
$
(
'.upload-modal .copy-button'
).
css
(
'display'
,
'inline-block'
);
$
(
'.upload-modal .copy-button'
).
css
(
'display'
,
'inline-block'
);
$
(
'.upload-modal .progress-bar'
).
addClass
(
'loaded'
);
$
(
'.upload-modal .progress-bar'
).
addClass
(
'loaded'
);
...
...
cms/static/js/models/feedback.js
View file @
319eb0ba
...
@@ -42,6 +42,12 @@ CMS.Models.ErrorMessage = CMS.Models.SystemFeedback.extend({
...
@@ -42,6 +42,12 @@ CMS.Models.ErrorMessage = CMS.Models.SystemFeedback.extend({
})
})
});
});
CMS
.
Models
.
ConfirmAssetDeleteMessage
=
CMS
.
Models
.
SystemFeedback
.
extend
({
defaults
:
$
.
extend
({},
CMS
.
Models
.
SystemFeedback
.
prototype
.
defaults
,
{
"intent"
:
"warning"
})
});
CMS
.
Models
.
ConfirmationMessage
=
CMS
.
Models
.
SystemFeedback
.
extend
({
CMS
.
Models
.
ConfirmationMessage
=
CMS
.
Models
.
SystemFeedback
.
extend
({
defaults
:
$
.
extend
({},
CMS
.
Models
.
SystemFeedback
.
prototype
.
defaults
,
{
defaults
:
$
.
extend
({},
CMS
.
Models
.
SystemFeedback
.
prototype
.
defaults
,
{
"intent"
:
"confirmation"
"intent"
:
"confirmation"
...
...
cms/static/js/views/assets.js
0 → 100644
View file @
319eb0ba
$
(
document
).
ready
(
function
()
{
$
(
'.uploads .upload-button'
).
bind
(
'click'
,
showUploadModal
);
$
(
'.upload-modal .close-button'
).
bind
(
'click'
,
hideModal
);
$
(
'.upload-modal .choose-file-button'
).
bind
(
'click'
,
showFileSelectionMenu
);
$
(
'.remove-asset-button'
).
bind
(
'click'
,
removeAsset
);
});
function
removeAsset
(
e
){
e
.
preventDefault
();
var
that
=
this
;
var
msg
=
new
CMS
.
Models
.
ConfirmAssetDeleteMessage
({
title
:
gettext
(
"Delete File Confirmation"
),
message
:
gettext
(
"Are you sure you wish to delete this item. It cannot be reversed!
\
n
\
nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"
),
actions
:
{
primary
:
{
text
:
gettext
(
"OK"
),
click
:
function
(
view
)
{
// call the back-end to actually remove the asset
$
.
post
(
view
.
model
.
get
(
'remove_asset_url'
),
{
'location'
:
view
.
model
.
get
(
'asset_location'
)
},
function
()
{
// show the post-commit confirmation
$
(
".wrapper-alert-confirmation"
).
addClass
(
"is-shown"
).
attr
(
'aria-hidden'
,
'false'
);
view
.
model
.
get
(
'row_to_remove'
).
remove
();
analytics
.
track
(
'Deleted Asset'
,
{
'course'
:
course_location_analytics
,
'id'
:
view
.
model
.
get
(
'asset_location'
)
});
}
);
view
.
hide
();
}
},
secondary
:
[{
text
:
gettext
(
"Cancel"
),
click
:
function
(
view
)
{
view
.
hide
();
}
}]
},
remove_asset_url
:
$
(
'.asset-library'
).
data
(
'remove-asset-callback-url'
),
asset_location
:
$
(
this
).
closest
(
'tr'
).
data
(
'id'
),
row_to_remove
:
$
(
this
).
closest
(
'tr'
)
});
// workaround for now. We can't spawn multiple instances of the Prompt View
// so for now, a bit of hackery to just make sure we have a single instance
// note: confirm_delete_prompt is in asset_index.html
if
(
confirm_delete_prompt
===
null
)
confirm_delete_prompt
=
new
CMS
.
Views
.
Prompt
({
model
:
msg
});
else
{
confirm_delete_prompt
.
model
=
msg
;
confirm_delete_prompt
.
show
();
}
return
;
}
function
showUploadModal
(
e
)
{
e
.
preventDefault
();
$modal
=
$
(
'.upload-modal'
).
show
();
$
(
'.file-input'
).
bind
(
'change'
,
startUpload
);
$modalCover
.
show
();
}
function
showFileSelectionMenu
(
e
)
{
e
.
preventDefault
();
$
(
'.file-input'
).
click
();
}
function
startUpload
(
e
)
{
var
files
=
$
(
'.file-input'
).
get
(
0
).
files
;
if
(
files
.
length
===
0
)
return
;
$
(
'.upload-modal h1'
).
html
(
gettext
(
'Uploading…'
));
$
(
'.upload-modal .file-name'
).
html
(
files
[
0
].
name
);
$
(
'.upload-modal .file-chooser'
).
ajaxSubmit
({
beforeSend
:
resetUploadBar
,
uploadProgress
:
showUploadFeedback
,
complete
:
displayFinishedUpload
});
$
(
'.upload-modal .choose-file-button'
).
hide
();
$
(
'.upload-modal .progress-bar'
).
removeClass
(
'loaded'
).
show
();
}
function
resetUploadBar
()
{
var
percentVal
=
'0%'
;
$
(
'.upload-modal .progress-fill'
).
width
(
percentVal
);
$
(
'.upload-modal .progress-fill'
).
html
(
percentVal
);
}
function
showUploadFeedback
(
event
,
position
,
total
,
percentComplete
)
{
var
percentVal
=
percentComplete
+
'%'
;
$
(
'.upload-modal .progress-fill'
).
width
(
percentVal
);
$
(
'.upload-modal .progress-fill'
).
html
(
percentVal
);
}
function
displayFinishedUpload
(
xhr
)
{
if
(
xhr
.
status
==
200
)
{
markAsLoaded
();
}
var
resp
=
JSON
.
parse
(
xhr
.
responseText
);
$
(
'.upload-modal .embeddable-xml-input'
).
val
(
xhr
.
getResponseHeader
(
'asset_url'
));
$
(
'.upload-modal .embeddable'
).
show
();
$
(
'.upload-modal .file-name'
).
hide
();
$
(
'.upload-modal .progress-fill'
).
html
(
resp
.
msg
);
$
(
'.upload-modal .choose-file-button'
).
html
(
gettext
(
'Load Another File'
)).
show
();
$
(
'.upload-modal .progress-fill'
).
width
(
'100%'
);
// see if this id already exists, if so, then user must have updated an existing piece of content
$
(
"tr[data-id='"
+
resp
.
url
+
"']"
).
remove
();
var
template
=
$
(
'#new-asset-element'
).
html
();
var
html
=
Mustache
.
to_html
(
template
,
resp
);
$
(
'table > tbody'
).
prepend
(
html
);
// re-bind the listeners to delete it
$
(
'.remove-asset-button'
).
bind
(
'click'
,
removeAsset
);
analytics
.
track
(
'Uploaded a File'
,
{
'course'
:
course_location_analytics
,
'asset_url'
:
resp
.
url
});
}
\ No newline at end of file
cms/static/sass/views/_assets.scss
View file @
319eb0ba
...
@@ -76,6 +76,10 @@ body.course.uploads {
...
@@ -76,6 +76,10 @@ body.course.uploads {
width
:
250px
;
width
:
250px
;
}
}
.delete-col
{
width
:
20px
;
}
.embeddable-xml-input
{
.embeddable-xml-input
{
@include
box-shadow
(
none
);
@include
box-shadow
(
none
);
width
:
100%
;
width
:
100%
;
...
...
cms/templates/asset_index.html
View file @
319eb0ba
<
%
inherit
file=
"base.html"
/>
<
%
inherit
file=
"base.html"
/>
<
%!
from
django
.
core
.
urlresolvers
import
reverse
%
>
<
%!
from
django
.
core
.
urlresolvers
import
reverse
%
>
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
%
>
<
%
block
name=
"bodyclass"
>
is-signedin course uploads
</
%
block>
<
%
block
name=
"bodyclass"
>
is-signedin course uploads
</
%
block>
<
%
block
name=
"title"
>
Files
&
Uploads
</
%
block>
<
%
block
name=
"title"
>
Files
&
Uploads
</
%
block>
...
@@ -7,6 +8,12 @@
...
@@ -7,6 +8,12 @@
<
%
block
name=
"jsextra"
>
<
%
block
name=
"jsextra"
>
<script
src=
"${static.url('js/vendor/mustache.js')}"
></script>
<script
src=
"${static.url('js/vendor/mustache.js')}"
></script>
<script
type=
"text/javascript"
src=
"${static.url('js/views/assets.js')}"
></script>
<script
type=
'text/javascript'
>
// we just want a singleton
confirm_delete_prompt
=
null
;
</script>
</
%
block>
</
%
block>
<
%
block
name=
"content"
>
<
%
block
name=
"content"
>
...
@@ -30,6 +37,9 @@
...
@@ -30,6 +37,9 @@
<
td
class
=
"embed-col"
>
<
td
class
=
"embed-col"
>
<
input
type
=
"text"
class
=
"embeddable-xml-input"
value
=
'{{url}}'
readonly
>
<
input
type
=
"text"
class
=
"embeddable-xml-input"
value
=
'{{url}}'
readonly
>
<
/td
>
<
/td
>
<
td
class
=
"delete-col"
>
<
a
href
=
"#"
data
-
tooltip
=
"${_('Delete this asset')}"
class
=
"remove-asset-button"
><
span
class
=
"delete-icon"
><
/span></
a
>
<
/td
>
<
/tr
>
<
/tr
>
</script>
</script>
...
@@ -56,7 +66,7 @@
...
@@ -56,7 +66,7 @@
<div
class=
"page-actions"
>
<div
class=
"page-actions"
>
<input
type=
"text"
class=
"asset-search-input search wip-box"
placeholder=
"search assets"
style=
"display:none"
/>
<input
type=
"text"
class=
"asset-search-input search wip-box"
placeholder=
"search assets"
style=
"display:none"
/>
</div>
</div>
<article
class=
"asset-library"
>
<article
class=
"asset-library"
data-remove-asset-callback-url=
'${remove_asset_callback_url}'
>
<table>
<table>
<thead>
<thead>
<tr>
<tr>
...
@@ -64,6 +74,7 @@
...
@@ -64,6 +74,7 @@
<th
class=
"name-col"
>
Name
</th>
<th
class=
"name-col"
>
Name
</th>
<th
class=
"date-col"
>
Date Added
</th>
<th
class=
"date-col"
>
Date Added
</th>
<th
class=
"embed-col"
>
URL
</th>
<th
class=
"embed-col"
>
URL
</th>
<th
class=
"delete-col"
></th>
</tr>
</tr>
</thead>
</thead>
<tbody
id=
"asset_table_body"
>
<tbody
id=
"asset_table_body"
>
...
@@ -86,6 +97,9 @@
...
@@ -86,6 +97,9 @@
<td
class=
"embed-col"
>
<td
class=
"embed-col"
>
<input
type=
"text"
class=
"embeddable-xml-input"
value=
"${asset['url']}"
readonly
>
<input
type=
"text"
class=
"embeddable-xml-input"
value=
"${asset['url']}"
readonly
>
</td>
</td>
<td
class=
"delete-col"
>
<a
href=
"#"
data-tooltip=
"${_('Delete this asset')}"
class=
"remove-asset-button"
><span
class=
"delete-icon"
></span></a>
</td>
</tr>
</tr>
% endfor
% endfor
</tbody>
</tbody>
...
@@ -129,3 +143,21 @@
...
@@ -129,3 +143,21 @@
</
%
block>
</
%
block>
<
%
block
name=
"view_alerts"
>
<!-- alert: save confirmed with close -->
<div
class=
"wrapper wrapper-alert wrapper-alert-confirmation"
role=
"status"
>
<div
class=
"alert confirmation"
>
<i
class=
"icon-ok"
></i>
<div
class=
"copy"
>
<h2
class=
"title title-3"
>
${_('Your file has been deleted.')}
</h2>
</div>
<a
href=
""
rel=
"view"
class=
"action action-alert-close"
>
<i
class=
"icon-remove-sign"
></i>
<span
class=
"label"
>
${_('close alert')}
</span>
</a>
</div>
</div>
</
%
block>
cms/urls.py
View file @
319eb0ba
...
@@ -35,6 +35,8 @@ urlpatterns = ('', # nopep8
...
@@ -35,6 +35,8 @@ urlpatterns = ('', # nopep8
'contentstore.views.preview_dispatch'
,
name
=
'preview_dispatch'
),
'contentstore.views.preview_dispatch'
,
name
=
'preview_dispatch'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$'
,
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$'
,
'contentstore.views.upload_asset'
,
name
=
'upload_asset'
),
'contentstore.views.upload_asset'
,
name
=
'upload_asset'
),
url
(
r'^manage_users/(?P<location>.*?)$'
,
'contentstore.views.manage_users'
,
name
=
'manage_users'
),
url
(
r'^manage_users/(?P<location>.*?)$'
,
'contentstore.views.manage_users'
,
name
=
'manage_users'
),
url
(
r'^add_user/(?P<location>.*?)$'
,
url
(
r'^add_user/(?P<location>.*?)$'
,
'contentstore.views.add_user'
,
name
=
'add_user'
),
'contentstore.views.add_user'
,
name
=
'add_user'
),
...
@@ -71,8 +73,11 @@ urlpatterns = ('', # nopep8
...
@@ -71,8 +73,11 @@ urlpatterns = ('', # nopep8
'contentstore.views.edit_static'
,
name
=
'edit_static'
),
'contentstore.views.edit_static'
,
name
=
'edit_static'
),
url
(
r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$'
,
url
(
r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$'
,
'contentstore.views.edit_tabs'
,
name
=
'edit_tabs'
),
'contentstore.views.edit_tabs'
,
name
=
'edit_tabs'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$'
,
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$'
,
'contentstore.views.asset_index'
,
name
=
'asset_index'
),
'contentstore.views.asset_index'
,
name
=
'asset_index'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)/remove$'
,
'contentstore.views.assets.remove_asset'
,
name
=
'remove_asset'
),
# this is a generic method to return the data/metadata associated with a xmodule
# this is a generic method to return the data/metadata associated with a xmodule
url
(
r'^module_info/(?P<module_location>.*)$'
,
url
(
r'^module_info/(?P<module_location>.*)$'
,
...
...
common/lib/xmodule/xmodule/contentstore/django.py
View file @
319eb0ba
...
@@ -3,7 +3,7 @@ from importlib import import_module
...
@@ -3,7 +3,7 @@ from importlib import import_module
from
django.conf
import
settings
from
django.conf
import
settings
_CONTENTSTORE
=
None
_CONTENTSTORE
=
{}
def
load_function
(
path
):
def
load_function
(
path
):
...
@@ -17,13 +17,16 @@ def load_function(path):
...
@@ -17,13 +17,16 @@ def load_function(path):
return
getattr
(
import_module
(
module_path
),
name
)
return
getattr
(
import_module
(
module_path
),
name
)
def
contentstore
():
def
contentstore
(
name
=
'default'
):
global
_CONTENTSTORE
global
_CONTENTSTORE
if
_CONTENTSTORE
is
None
:
if
name
not
in
_CONTENTSTORE
:
class_
=
load_function
(
settings
.
CONTENTSTORE
[
'ENGINE'
])
class_
=
load_function
(
settings
.
CONTENTSTORE
[
'ENGINE'
])
options
=
{}
options
=
{}
options
.
update
(
settings
.
CONTENTSTORE
[
'OPTIONS'
])
options
.
update
(
settings
.
CONTENTSTORE
[
'OPTIONS'
])
_CONTENTSTORE
=
class_
(
**
options
)
if
'ADDITIONAL_OPTIONS'
in
settings
.
CONTENTSTORE
:
if
name
in
settings
.
CONTENTSTORE
[
'ADDITIONAL_OPTIONS'
]:
options
.
update
(
settings
.
CONTENTSTORE
[
'ADDITIONAL_OPTIONS'
][
name
])
_CONTENTSTORE
[
name
]
=
class_
(
**
options
)
return
_CONTENTSTORE
return
_CONTENTSTORE
[
name
]
common/lib/xmodule/xmodule/contentstore/mongo.py
View file @
319eb0ba
from
bson.son
import
SON
from
pymongo
import
Connection
from
pymongo
import
Connection
import
gridfs
import
gridfs
from
gridfs.errors
import
NoFile
from
gridfs.errors
import
NoFile
...
@@ -15,15 +14,16 @@ import os
...
@@ -15,15 +14,16 @@ import os
class
MongoContentStore
(
ContentStore
):
class
MongoContentStore
(
ContentStore
):
def
__init__
(
self
,
host
,
db
,
port
=
27017
,
user
=
None
,
password
=
None
,
**
kwargs
):
def
__init__
(
self
,
host
,
db
,
port
=
27017
,
user
=
None
,
password
=
None
,
bucket
=
'fs'
,
**
kwargs
):
logging
.
debug
(
'Using MongoDB for static content serving at host={0} db={1}'
.
format
(
host
,
db
))
logging
.
debug
(
'Using MongoDB for static content serving at host={0} db={1}'
.
format
(
host
,
db
))
_db
=
Connection
(
host
=
host
,
port
=
port
,
**
kwargs
)[
db
]
_db
=
Connection
(
host
=
host
,
port
=
port
,
**
kwargs
)[
db
]
if
user
is
not
None
and
password
is
not
None
:
if
user
is
not
None
and
password
is
not
None
:
_db
.
authenticate
(
user
,
password
)
_db
.
authenticate
(
user
,
password
)
self
.
fs
=
gridfs
.
GridFS
(
_db
)
self
.
fs
=
gridfs
.
GridFS
(
_db
,
bucket
)
self
.
fs_files
=
_db
[
"fs.files"
]
# the underlying collection GridFS uses
self
.
fs_files
=
_db
[
bucket
+
".files"
]
# the underlying collection GridFS uses
def
save
(
self
,
content
):
def
save
(
self
,
content
):
id
=
content
.
get_id
()
id
=
content
.
get_id
()
...
@@ -43,7 +43,7 @@ class MongoContentStore(ContentStore):
...
@@ -43,7 +43,7 @@ class MongoContentStore(ContentStore):
if
self
.
fs
.
exists
({
"_id"
:
id
}):
if
self
.
fs
.
exists
({
"_id"
:
id
}):
self
.
fs
.
delete
(
id
)
self
.
fs
.
delete
(
id
)
def
find
(
self
,
location
):
def
find
(
self
,
location
,
throw_on_not_found
=
True
):
id
=
StaticContent
.
get_id_from_location
(
location
)
id
=
StaticContent
.
get_id_from_location
(
location
)
try
:
try
:
with
self
.
fs
.
get
(
id
)
as
fp
:
with
self
.
fs
.
get
(
id
)
as
fp
:
...
@@ -52,7 +52,10 @@ class MongoContentStore(ContentStore):
...
@@ -52,7 +52,10 @@ class MongoContentStore(ContentStore):
thumbnail_location
=
fp
.
thumbnail_location
if
hasattr
(
fp
,
'thumbnail_location'
)
else
None
,
thumbnail_location
=
fp
.
thumbnail_location
if
hasattr
(
fp
,
'thumbnail_location'
)
else
None
,
import_path
=
fp
.
import_path
if
hasattr
(
fp
,
'import_path'
)
else
None
)
import_path
=
fp
.
import_path
if
hasattr
(
fp
,
'import_path'
)
else
None
)
except
NoFile
:
except
NoFile
:
raise
NotFoundError
()
if
throw_on_not_found
:
raise
NotFoundError
()
else
:
return
None
def
export
(
self
,
location
,
output_directory
):
def
export
(
self
,
location
,
output_directory
):
content
=
self
.
find
(
location
)
content
=
self
.
find
(
location
)
...
...
common/lib/xmodule/xmodule/contentstore/utils.py
0 → 100644
View file @
319eb0ba
from
xmodule.modulestore
import
Location
from
xmodule.contentstore.content
import
StaticContent
from
.django
import
contentstore
def
empty_asset_trashcan
(
course_locs
):
'''
This method will hard delete all assets (optionally within a course_id) from the trashcan
'''
store
=
contentstore
(
'trashcan'
)
for
course_loc
in
course_locs
:
# first delete all of the thumbnails
thumbs
=
store
.
get_all_content_thumbnails_for_course
(
course_loc
)
for
thumb
in
thumbs
:
thumb_loc
=
Location
(
thumb
[
"_id"
])
id
=
StaticContent
.
get_id_from_location
(
thumb_loc
)
print
"Deleting {0}..."
.
format
(
id
)
store
.
delete
(
id
)
# then delete all of the assets
assets
=
store
.
get_all_content_for_course
(
course_loc
)
for
asset
in
assets
:
asset_loc
=
Location
(
asset
[
"_id"
])
id
=
StaticContent
.
get_id_from_location
(
asset_loc
)
print
"Deleting {0}..."
.
format
(
id
)
store
.
delete
(
id
)
def
restore_asset_from_trashcan
(
location
):
'''
This method will restore an asset which got soft deleted and put back in the original course
'''
trash
=
contentstore
(
'trashcan'
)
store
=
contentstore
()
loc
=
StaticContent
.
get_location_from_path
(
location
)
content
=
trash
.
find
(
loc
)
# ok, save the content into the courseware
store
.
save
(
content
)
# see if there is a thumbnail as well, if so move that as well
if
content
.
thumbnail_location
is
not
None
:
try
:
thumbnail_content
=
trash
.
find
(
content
.
thumbnail_location
)
store
.
save
(
thumbnail_content
)
except
:
pass
# OK if this is left dangling
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