Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
problem-builder
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
OpenEdx
problem-builder
Commits
9cedb7cb
Commit
9cedb7cb
authored
Jul 19, 2015
by
Xavier Antoviaque
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'dropdown-pagination' into dropdown-pagination-rebased
parents
4a9a6d44
46dab9e3
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
320 additions
and
68 deletions
+320
-68
problem_builder/instructor_tool.py
+116
-6
problem_builder/public/css/instructor_tool.css
+7
-1
problem_builder/public/js/instructor_tool.js
+148
-44
problem_builder/tasks.py
+3
-12
problem_builder/templates/html/instructor_tool.html
+10
-2
problem_builder/tests/integration/test_instructor_tool.py
+36
-3
No files found.
problem_builder/instructor_tool.py
View file @
9cedb7cb
...
...
@@ -23,14 +23,17 @@ Instructor Tool: An XBlock for instructors to export student answers from a cour
All processing is done offline.
"""
import
json
from
django.core.paginator
import
Paginator
from
xblock.core
import
XBlock
from
xblock.exceptions
import
JsonHandlerError
from
xblock.fields
import
Scope
,
String
,
Dict
from
xblock.fields
import
Scope
,
String
,
Dict
,
List
from
xblock.fragment
import
Fragment
from
xblockutils.resources
import
ResourceLoader
loader
=
ResourceLoader
(
__name__
)
PAGE_SIZE
=
15
# Make '_' a no-op so we can scrape strings
def
_
(
text
):
...
...
@@ -63,6 +66,12 @@ class InstructorToolBlock(XBlock):
default
=
None
,
scope
=
Scope
.
user_state
,
)
display_data
=
List
(
# The list of results associated with the most recent successful export.
# Stored separately to avoid the overhead of sending it to the client.
default
=
None
,
scope
=
Scope
.
user_state
,
)
has_author_view
=
True
@property
...
...
@@ -75,6 +84,11 @@ class InstructorToolBlock(XBlock):
# different celery queues; our task listener is waiting for tasks on the LMS queue)
return
Fragment
(
u'<p>Instructor Tool Block</p><p>This block only works from the LMS.</p>'
)
def
studio_view
(
self
,
context
=
None
):
""" View for editing Instructor Tool block in Studio. """
# Display friendly message explaining that the block is not editable.
return
Fragment
(
u'<p>This is a preconfigured block. It is not editable.</p>'
)
def
check_pending_export
(
self
):
"""
If we're waiting for an export, see if it has finished, and if so, get the result.
...
...
@@ -90,11 +104,26 @@ class InstructorToolBlock(XBlock):
self
.
active_export_task_id
=
''
if
task_result
.
successful
():
if
isinstance
(
task_result
.
result
,
dict
)
and
not
task_result
.
result
.
get
(
'error'
):
self
.
display_data
=
task_result
.
result
[
'display_data'
]
del
task_result
.
result
[
'display_data'
]
self
.
last_export_result
=
task_result
.
result
else
:
self
.
last_export_result
=
{
'error'
:
u'Unexpected result: {}'
.
format
(
repr
(
task_result
.
result
))}
self
.
display_data
=
None
else
:
self
.
last_export_result
=
{
'error'
:
unicode
(
task_result
.
result
)}
self
.
display_data
=
None
@XBlock.json_handler
def
get_result_page
(
self
,
data
,
suffix
=
''
):
""" Return requested page of `last_export_result`. """
paginator
=
Paginator
(
self
.
display_data
,
PAGE_SIZE
)
page
=
data
.
get
(
'page'
,
None
)
return
{
'display_data'
:
paginator
.
page
(
page
)
.
object_list
,
'num_results'
:
len
(
self
.
display_data
),
'page_size'
:
PAGE_SIZE
}
def
student_view
(
self
,
context
=
None
):
""" Normal View """
...
...
@@ -105,9 +134,92 @@ class InstructorToolBlock(XBlock):
_
(
'Rating Question'
):
'RatingBlock'
,
_
(
'Long Answer'
):
'AnswerBlock'
,
}
block_types
=
(
'pb-mcq'
,
'pb-rating'
,
'pb-answer'
)
flat_block_tree
=
[]
def
get_block_id
(
block
):
"""
Return ID of `block`, taking into account needs of both LMS/CMS and workbench runtimes.
"""
usage_id
=
block
.
scope_ids
.
usage_id
# Try accessing block ID. If usage_id does not have it, return usage_id itself
return
unicode
(
getattr
(
usage_id
,
'block_id'
,
usage_id
))
def
get_block_name
(
block
):
"""
Return name of `block`.
Try attributes in the following order:
- block.question
- block.name (fallback for old courses)
- block.display_name
- block ID
"""
# - Try "question" attribute:
block_name
=
getattr
(
block
,
'question'
,
block
.
name
)
if
not
block_name
:
# - Try display_name:
block_name
=
getattr
(
block
,
'display_name'
,
None
)
if
not
block_name
:
# - Default to ID:
block_name
=
get_block_id
(
block
)
return
block_name
def
get_block_type
(
block
):
"""
Return type of `block`, taking into account different key styles that might be in use.
"""
try
:
block_type
=
block
.
runtime
.
id_reader
.
get_block_type
(
block
.
scope_ids
.
def_id
)
except
AttributeError
:
block_type
=
block
.
runtime
.
id_reader
.
get_block_type
(
block
.
scope_ids
.
usage_id
)
return
block_type
def
build_tree
(
block
,
ancestors
):
"""
Build up a tree of information about the XBlocks descending from root_block
"""
block_id
=
get_block_id
(
block
)
block_name
=
get_block_name
(
block
)
block_type
=
get_block_type
(
block
)
if
not
block_type
==
'pb-choice'
:
eligible
=
block_type
in
block_types
if
eligible
:
# If this block is a question whose answers we can export,
# we mark all of its ancestors as exportable too
if
ancestors
and
not
ancestors
[
-
1
][
"eligible"
]:
for
ancestor
in
ancestors
:
ancestor
[
"eligible"
]
=
True
new_entry
=
{
"depth"
:
len
(
ancestors
),
"id"
:
block_id
,
"name"
:
block_name
,
"eligible"
:
eligible
,
}
flat_block_tree
.
append
(
new_entry
)
if
block
.
has_children
and
not
getattr
(
block
,
"has_dynamic_children"
,
lambda
:
False
)():
for
child_id
in
block
.
children
:
build_tree
(
block
.
runtime
.
get_block
(
child_id
),
ancestors
=
(
ancestors
+
[
new_entry
]))
root_block
=
self
while
root_block
.
parent
:
root_block
=
root_block
.
get_parent
()
root_block_id
=
get_block_id
(
root_block
)
root_entry
=
{
"depth"
:
0
,
"id"
:
root_block_id
,
"name"
:
"All"
,
}
flat_block_tree
.
append
(
root_entry
)
for
child_id
in
root_block
.
children
:
child_block
=
root_block
.
runtime
.
get_block
(
child_id
)
build_tree
(
child_block
,
[
root_entry
])
html
=
loader
.
render_template
(
'templates/html/instructor_tool.html'
,
{
'block_choices'
:
block_choices
}
{
'block_choices'
:
block_choices
,
'block_tree'
:
flat_block_tree
}
)
fragment
=
Fragment
(
html
)
fragment
.
add_css_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/css/instructor_tool.css'
))
...
...
@@ -144,6 +256,7 @@ class InstructorToolBlock(XBlock):
self
.
last_export_result
=
{
'error'
:
message
,
}
self
.
display_data
=
None
raise
JsonHandlerError
(
code
,
message
)
@XBlock.json_handler
...
...
@@ -157,6 +270,7 @@ class InstructorToolBlock(XBlock):
def
_delete_export
(
self
):
self
.
last_export_result
=
None
self
.
display_data
=
None
self
.
active_export_task_id
=
''
@XBlock.json_handler
...
...
@@ -187,9 +301,6 @@ class InstructorToolBlock(XBlock):
root_block_id
=
self
.
scope_ids
.
usage_id
# Block ID not in workbench runtime.
root_block_id
=
unicode
(
getattr
(
root_block_id
,
'block_id'
,
root_block_id
))
get_root
=
True
else
:
get_root
=
False
# Launch task
from
.tasks
import
export_data
as
export_data_task
# Import here since this is edX LMS specific
...
...
@@ -203,7 +314,6 @@ class InstructorToolBlock(XBlock):
block_types
,
user_id
,
match_string
,
get_root
=
get_root
)
if
async_result
.
ready
():
# In development mode, the task may have executed synchronously.
...
...
problem_builder/public/css/instructor_tool.css
View file @
9cedb7cb
...
...
@@ -25,6 +25,12 @@
display
:
table-cell
;
padding-left
:
1em
;
}
.data-export-field-container
{
width
:
43%
;
}
.data-export-options
.data-export-actions
{
max-width
:
10%
;
}
.data-export-field
{
margin-top
:
.5em
;
margin-bottom
:
.5em
;
...
...
@@ -34,7 +40,7 @@
vertical-align
:
middle
;
}
.data-export-field
input
,
.data-export-field
select
{
max-width
:
60
%
;
width
:
55
%
;
float
:
right
;
}
.data-export-results
,
.data-export-download
,
.data-export-cancel
,
.data-export-delete
{
...
...
problem_builder/public/js/instructor_tool.js
View file @
9cedb7cb
...
...
@@ -4,6 +4,12 @@ function InstructorToolBlock(runtime, element) {
// Pagination
$
(
document
).
ajaxSend
(
function
(
event
,
jqxhr
,
options
)
{
if
(
options
.
url
.
indexOf
(
'get_result_page'
)
!==
-
1
)
{
options
.
data
=
JSON
.
stringify
(
options
.
data
);
}
});
var
Result
=
Backbone
.
Model
.
extend
({
initialize
:
function
(
attrs
,
options
)
{
...
...
@@ -18,12 +24,54 @@ function InstructorToolBlock(runtime, element) {
model
:
Result
,
getCurrentPage
:
function
(
returnObject
)
{
var
currentPage
=
this
.
state
.
currentPage
;
if
(
returnObject
)
{
return
this
.
getPage
(
currentPage
);
}
return
currentPage
;
state
:
{
order
:
0
},
url
:
runtime
.
handlerUrl
(
element
,
'get_result_page'
),
parseState
:
function
(
response
)
{
return
{
totalRecords
:
response
.
num_results
,
pageSize
:
response
.
page_size
};
},
parseRecords
:
function
(
response
)
{
return
_
.
map
(
response
.
display_data
,
function
(
row
)
{
return
new
Result
(
null
,
{
values
:
row
});
});
},
fetchOptions
:
{
reset
:
true
,
type
:
'POST'
,
contentType
:
'application/json'
,
processData
:
false
},
getFirstPage
:
function
()
{
Backbone
.
PageableCollection
.
prototype
.
getFirstPage
.
call
(
this
,
this
.
fetchOptions
);
},
getPreviousPage
:
function
()
{
Backbone
.
PageableCollection
.
prototype
.
getPreviousPage
.
call
(
this
,
this
.
fetchOptions
);
},
getNextPage
:
function
()
{
Backbone
.
PageableCollection
.
prototype
.
getNextPage
.
call
(
this
,
this
.
fetchOptions
);
},
getLastPage
:
function
()
{
Backbone
.
PageableCollection
.
prototype
.
getLastPage
.
call
(
this
,
this
.
fetchOptions
);
},
getCurrentPage
:
function
()
{
return
this
.
state
.
currentPage
;
},
getTotalPages
:
function
()
{
...
...
@@ -34,17 +82,26 @@ function InstructorToolBlock(runtime, element) {
var
ResultsView
=
Backbone
.
View
.
extend
({
initialize
:
function
()
{
this
.
listenTo
(
this
.
collection
,
'reset'
,
this
.
render
);
this
.
listenTo
(
this
,
'rendered'
,
this
.
_show
);
this
.
listenTo
(
this
,
'processing'
,
this
.
_hide
);
this
.
listenTo
(
this
,
'error'
,
this
.
_hide
);
this
.
listenTo
(
this
,
'update'
,
this
.
_updateInfo
);
},
render
:
function
()
{
this
.
_insertRecords
(
this
.
collection
.
getCurrentPage
(
true
)
);
this
.
_insertRecords
();
this
.
_updateControls
();
this
.
$
(
'#total-pages'
).
text
(
this
.
collection
.
getTotalPages
()
||
0
);
this
.
trigger
(
'rendered'
);
return
this
;
},
_insertRecords
:
function
(
records
)
{
_insertRecords
:
function
()
{
var
tbody
=
this
.
$
(
'tbody'
);
tbody
.
empty
();
records
.
each
(
function
(
result
,
index
)
{
this
.
collection
.
each
(
function
(
result
,
index
)
{
var
row
=
$
(
'<tr>'
);
_
.
each
(
Result
.
properties
,
function
(
name
)
{
row
.
append
(
$
(
'<td>'
).
text
(
result
.
get
(
name
)));
...
...
@@ -58,6 +115,20 @@ function InstructorToolBlock(runtime, element) {
}
},
_show
:
function
()
{
this
.
$el
.
show
(
700
);
},
_hide
:
function
()
{
this
.
$el
.
hide
();
},
_updateInfo
:
function
(
info
)
{
var
$exportInfo
=
this
.
$
(
'.data-export-info'
);
$exportInfo
.
empty
();
$exportInfo
.
append
(
$
(
'<p>'
).
text
(
info
));
},
events
:
{
'click #first-page'
:
'_firstPage'
,
'click #prev-page'
:
'_prevPage'
,
...
...
@@ -66,26 +137,26 @@ function InstructorToolBlock(runtime, element) {
},
_firstPage
:
function
()
{
this
.
_insertRecords
(
this
.
collection
.
getFirstPage
()
);
this
.
collection
.
getFirstPage
(
);
this
.
_updateControls
();
},
_prevPage
:
function
()
{
if
(
this
.
collection
.
hasPreviousPage
())
{
this
.
_insertRecords
(
this
.
collection
.
getPreviousPage
()
);
this
.
collection
.
getPreviousPage
(
);
}
this
.
_updateControls
();
},
_nextPage
:
function
()
{
if
(
this
.
collection
.
hasNextPage
())
{
this
.
_insertRecords
(
this
.
collection
.
getNextPage
()
);
this
.
collection
.
getNextPage
(
);
}
this
.
_updateControls
();
},
_lastPage
:
function
()
{
this
.
_insertRecords
(
this
.
collection
.
getLastPage
()
);
this
.
collection
.
getLastPage
(
);
this
.
_updateControls
();
},
...
...
@@ -107,10 +178,42 @@ function InstructorToolBlock(runtime, element) {
});
var
resultsView
=
new
ResultsView
({
collection
:
new
Results
([]
,
{
mode
:
"client"
,
state
:
{
pageSize
:
15
}
}
),
collection
:
new
Results
([]),
el
:
$element
.
find
(
'#results'
)
});
// Status area
var
StatusView
=
Backbone
.
View
.
extend
({
initialize
:
function
()
{
this
.
listenTo
(
this
,
'processing'
,
this
.
_showSpinner
);
this
.
listenTo
(
this
,
'notify'
,
this
.
_displayMessage
);
this
.
listenTo
(
this
,
'stopped'
,
this
.
_empty
);
this
.
listenTo
(
resultsView
,
'rendered'
,
this
.
_empty
);
},
_showSpinner
:
function
()
{
this
.
$el
.
empty
();
this
.
$el
.
append
(
$
(
'<i>'
).
addClass
(
'icon fa fa-spinner fa-spin'
)
).
css
(
'text-align'
,
'center'
);
},
_displayMessage
:
function
(
message
)
{
this
.
$el
.
append
(
$
(
'<p>'
).
text
(
message
));
},
_empty
:
function
()
{
this
.
$el
.
empty
();
}
});
var
statusView
=
new
StatusView
({
el
:
$element
.
find
(
'.data-export-status'
)
});
// Set up gettext in case it isn't available in the client runtime:
if
(
typeof
gettext
==
"undefined"
)
{
window
.
gettext
=
function
gettext_stub
(
string
)
{
return
string
;
};
...
...
@@ -121,7 +224,7 @@ function InstructorToolBlock(runtime, element) {
var
$downloadButton
=
$element
.
find
(
'.data-export-download'
);
var
$deleteButton
=
$element
.
find
(
'.data-export-delete'
);
var
$blockTypes
=
$element
.
find
(
"select[name='block_types']"
);
var
$rootBlockId
=
$element
.
find
(
"
inpu
t[name='root_block_id']"
);
var
$rootBlockId
=
$element
.
find
(
"
selec
t[name='root_block_id']"
);
var
$username
=
$element
.
find
(
"input[name='username']"
);
var
$matchString
=
$element
.
find
(
"input[name='match_string']"
);
var
$resultTable
=
$element
.
find
(
'.data-export-results'
);
...
...
@@ -147,18 +250,15 @@ function InstructorToolBlock(runtime, element) {
if
(
statusChanged
)
updateView
();
}
function
showSpinner
()
{
function
disableActions
()
{
$startButton
.
prop
(
'disabled'
,
true
);
$cancelButton
.
prop
(
'disabled'
,
true
);
$downloadButton
.
prop
(
'disabled'
,
true
);
$deleteButton
.
prop
(
'disabled'
,
true
);
$
(
'.data-export-status'
,
$element
).
empty
().
append
(
$
(
'<i>'
).
addClass
(
'icon fa fa-spinner fa-spin'
)
).
css
(
"text-align"
,
"center"
);
}
function
hideResults
(
)
{
$resultTable
.
hide
(
);
function
showInfo
(
info
)
{
resultsView
.
trigger
(
'update'
,
info
);
}
function
showResults
()
{
...
...
@@ -167,6 +267,22 @@ function InstructorToolBlock(runtime, element) {
}
}
function
hideResults
()
{
resultsView
.
trigger
(
'processing'
);
}
function
showSpinner
()
{
statusView
.
trigger
(
'processing'
);
}
function
hideSpinner
()
{
statusView
.
trigger
(
'stopped'
);
}
function
showStatusMessage
(
message
)
{
statusView
.
trigger
(
'notify'
,
message
);
}
function
handleError
(
data
)
{
// Shim to make the XBlock JsonHandlerError response work with our format.
status
=
{
'last_export_result'
:
JSON
.
parse
(
data
.
responseText
),
'export_pending'
:
false
};
...
...
@@ -174,26 +290,22 @@ function InstructorToolBlock(runtime, element) {
}
function
updateView
()
{
var
$exportInfo
=
$
(
'.data-export-info'
,
$element
),
$statusArea
=
$
(
'.data-export-status'
,
$element
),
startTime
;
$statusArea
.
empty
();
$exportInfo
.
empty
();
var
startTime
;
$startButton
.
toggle
(
!
status
.
export_pending
).
prop
(
'disabled'
,
false
);
$cancelButton
.
toggle
(
status
.
export_pending
).
prop
(
'disabled'
,
false
);
$downloadButton
.
toggle
(
Boolean
(
status
.
download_url
)).
prop
(
'disabled'
,
false
);
$deleteButton
.
toggle
(
Boolean
(
status
.
last_export_result
)).
prop
(
'disabled'
,
false
);
if
(
status
.
last_export_result
)
{
if
(
status
.
last_export_result
.
error
)
{
$statusArea
.
append
(
$
(
'<p>'
).
text
(
_
.
template
(
hideResults
();
hideSpinner
();
showStatusMessage
(
_
.
template
(
gettext
(
'Data export failed. Reason: <%= error %>'
),
{
'error'
:
status
.
last_export_result
.
error
}
)
));
hideResults
();
}
else
{
startTime
=
new
Date
(
status
.
last_export_result
.
start_timestamp
*
1000
);
$exportInfo
.
append
(
$
(
'<p>'
).
text
(
showInfo
(
_
.
template
(
ngettext
(
'Results retrieved on <%= creation_time %> (<%= seconds %> second).'
,
...
...
@@ -204,24 +316,14 @@ function InstructorToolBlock(runtime, element) {
'creation_time'
:
startTime
.
toString
(),
'seconds'
:
status
.
last_export_result
.
generation_time_s
.
toFixed
(
1
)
}
)
));
// Display results
var
results
=
_
.
map
(
status
.
last_export_result
.
display_data
,
function
(
row
)
{
return
new
Result
(
null
,
{
values
:
row
});
});
resultsView
.
collection
.
fullCollection
.
reset
(
results
);
resultsView
.
render
();
showResults
();
resultsView
.
collection
.
getFirstPage
();
}
}
else
{
if
(
status
.
export_pending
)
{
$statusArea
.
append
(
$
(
'<p>'
).
text
(
gettext
(
'The report is currently being generated…'
)
)
);
showStatusMessage
(
gettext
(
'The report is currently being generated…'
));
}
else
{
hideSpinner
(
);
}
}
}
...
...
@@ -249,6 +351,7 @@ function InstructorToolBlock(runtime, element) {
dataType
:
'json'
});
showSpinner
();
disableActions
();
});
}
...
...
@@ -265,6 +368,7 @@ function InstructorToolBlock(runtime, element) {
});
showSpinner
();
disableActions
();
getStatus
();
}
problem_builder/tasks.py
View file @
9cedb7cb
...
...
@@ -6,7 +6,6 @@ import time
from
celery.task
import
task
from
celery.utils.log
import
get_task_logger
from
instructor_task.models
import
ReportStore
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
student.models
import
user_by_anonymous_id
from
xmodule.modulestore.django
import
modulestore
...
...
@@ -21,7 +20,7 @@ logger = get_task_logger(__name__)
@task
()
def
export_data
(
course_id
,
source_block_id_str
,
block_types
,
user_id
,
match_string
,
get_root
=
True
):
def
export_data
(
course_id
,
source_block_id_str
,
block_types
,
user_id
,
match_string
):
"""
Exports student answers to all MCQ questions to a CSV file.
"""
...
...
@@ -31,18 +30,10 @@ def export_data(course_id, source_block_id_str, block_types, user_id, match_stri
try
:
course_key
=
CourseKey
.
from_string
(
course_id
)
src_block
=
modulestore
()
.
get_items
(
course_key
,
qualifiers
=
{
'name'
:
source_block_id_str
},
depth
=
0
)[
0
]
if
src_block
is
None
:
raise
InvalidKeyError
except
InvalidKeyError
:
except
IndexError
:
raise
ValueError
(
"Could not find the specified Block ID."
)
course_key_str
=
unicode
(
course_key
)
root
=
src_block
if
get_root
:
# Get the root block for the course.
while
root
.
parent
:
root
=
root
.
get_parent
()
type_map
=
{
cls
.
__name__
:
cls
for
cls
in
[
MCQBlock
,
RatingBlock
,
AnswerBlock
]}
if
not
block_types
:
...
...
@@ -65,7 +56,7 @@ def export_data(course_id, source_block_id_str, block_types, user_id, match_stri
# Blocks may refer to missing children. Don't break in this case.
pass
scan_for_blocks
(
root
)
scan_for_blocks
(
src_block
)
# Define the header row of our CSV:
rows
=
[]
...
...
problem_builder/templates/html/instructor_tool.html
View file @
9cedb7cb
...
...
@@ -27,8 +27,16 @@
<div
class=
"data-export-field-container"
>
<div
class=
"data-export-field"
>
<label>
<span>
{% trans "Root block ID:" %}
</span>
<input
type=
"text"
name=
"root_block_id"
/>
<span>
{% trans "Section/Question:" %}
</span>
<select
name=
"root_block_id"
>
{% for block in block_tree %}
<option
value=
"{{ block.id }}"
{%
if
not
block
.
eligible
%}
disabled=
"disabled"
{%
endif
%}
>
{% for _ in ""|ljust:block.depth %}
{% endfor %}
{{ block.name }}
</option>
{% endfor %}
</select>
</label>
</div>
</div>
...
...
problem_builder/tests/integration/test_instructor_tool.py
View file @
9cedb7cb
...
...
@@ -7,7 +7,7 @@ from mock import patch, Mock
from
selenium.common.exceptions
import
NoSuchElementException
from
xblockutils.base_test
import
SeleniumXBlockTest
from
problem_builder.instructor_tool
import
InstructorToolBlock
from
problem_builder.instructor_tool
import
PAGE_SIZE
,
InstructorToolBlock
class
MockTasksModule
(
object
):
...
...
@@ -62,6 +62,36 @@ class InstructorToolTest(SeleniumXBlockTest):
'instructor_task.models'
:
MockInstructorTaskModelsModule
(),
})
@patch.object
(
InstructorToolBlock
,
'user_is_staff'
,
Mock
(
return_value
=
True
))
def
test_data_export_delete
(
self
):
instructor_tool
=
self
.
go_to_view
()
start_button
=
instructor_tool
.
find_element_by_class_name
(
'data-export-start'
)
result_block
=
instructor_tool
.
find_element_by_class_name
(
'data-export-results'
)
status_area
=
instructor_tool
.
find_element_by_class_name
(
'data-export-status'
)
download_button
=
instructor_tool
.
find_element_by_class_name
(
'data-export-download'
)
cancel_button
=
instructor_tool
.
find_element_by_class_name
(
'data-export-cancel'
)
delete_button
=
instructor_tool
.
find_element_by_class_name
(
'data-export-delete'
)
start_button
.
click
()
self
.
wait_until_visible
(
result_block
)
self
.
wait_until_visible
(
delete_button
)
delete_button
.
click
()
self
.
wait_until_hidden
(
result_block
)
self
.
wait_until_hidden
(
delete_button
)
self
.
assertTrue
(
start_button
.
is_enabled
())
self
.
assertEqual
(
''
,
status_area
.
text
)
self
.
assertFalse
(
download_button
.
is_displayed
())
self
.
assertFalse
(
cancel_button
.
is_displayed
())
@patch.dict
(
'sys.modules'
,
{
'problem_builder.tasks'
:
MockTasksModule
(
successful
=
True
),
'instructor_task'
:
True
,
'instructor_task.models'
:
MockInstructorTaskModelsModule
(),
})
@patch.object
(
InstructorToolBlock
,
'user_is_staff'
,
Mock
(
return_value
=
True
))
def
test_data_export_success
(
self
):
instructor_tool
=
self
.
go_to_view
()
start_button
=
instructor_tool
.
find_element_by_class_name
(
'data-export-start'
)
...
...
@@ -146,6 +176,7 @@ class InstructorToolTest(SeleniumXBlockTest):
start_button
.
click
()
self
.
wait_until_visible
(
result_block
)
time
.
sleep
(
1
)
# Allow some time for result block to fully fade in
self
.
assertFalse
(
first_page_button
.
is_enabled
())
self
.
assertFalse
(
prev_page_button
.
is_enabled
())
...
...
@@ -179,6 +210,7 @@ class InstructorToolTest(SeleniumXBlockTest):
start_button
.
click
()
self
.
wait_until_visible
(
result_block
)
time
.
sleep
(
1
)
# Allow some time for result block to fully fade in
for
contents
in
[
'Test section'
,
'Test subsection'
,
'Test unit'
,
...
...
@@ -199,7 +231,7 @@ class InstructorToolTest(SeleniumXBlockTest):
successful
=
True
,
display_data
=
[[
'Test section'
,
'Test subsection'
,
'Test unit'
,
'Test type'
,
'Test question'
,
'Test answer'
,
'Test username'
]
for
_
in
range
(
45
)]),
]
for
_
in
range
(
PAGE_SIZE
*
3
)]),
'instructor_task'
:
True
,
'instructor_task.models'
:
MockInstructorTaskModelsModule
(),
})
...
...
@@ -218,13 +250,14 @@ class InstructorToolTest(SeleniumXBlockTest):
start_button
.
click
()
self
.
wait_until_visible
(
result_block
)
time
.
sleep
(
1
)
# Allow some time for result block to fully fade in
for
contents
in
[
'Test section'
,
'Test subsection'
,
'Test unit'
,
'Test type'
,
'Test question'
,
'Test answer'
,
'Test username'
]:
occurrences
=
re
.
findall
(
contents
,
result_block
.
text
)
self
.
assertEqual
(
len
(
occurrences
),
15
)
self
.
assertEqual
(
len
(
occurrences
),
PAGE_SIZE
)
self
.
assertFalse
(
first_page_button
.
is_enabled
())
self
.
assertFalse
(
prev_page_button
.
is_enabled
())
...
...
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