1
+ #
2
+ # Copyright (c) 2014-2015 Harvard, edX & OpenCraft
3
+ #
4
+ # This software's license gives you freedom; you can copy, convey,
5
+ # propagate, redistribute and/or modify this program under the terms of
6
+ # the GNU Affero General Public License (AGPL) as published by the Free
7
+ # Software Foundation (FSF), either version 3 of the License, or (at your
8
+ # option) any later version of the AGPL published by the FSF.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but
11
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
13
+ # General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program in a file in the toplevel directory called
17
+ # "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+ """
20
+ Instructor Tool: An XBlock for instructors to export student answers from a course.
21
+
22
+ All processing is done offline.
23
+ """
24
+ import json
25
+ import pkg_resources
26
+
27
+ from django .utils .translation import gettext_noop as _
28
+ from web_fragments .fragment import Fragment
29
+ from xblock .core import XBlock
30
+ from xblock .exceptions import JsonHandlerError
31
+ from xblock .fields import Dict , Scope , String
32
+ from xblock .utils .resources import ResourceLoader
33
+
34
+ loader = ResourceLoader (__name__ )
35
+
36
+
37
+ @XBlock .wants ('user' )
38
+ class DataExportXBlock (XBlock ):
39
+ icon_class = "problem"
40
+ display_name = String (
41
+ display_name = _ ("(Display name)" ),
42
+ help = _ ("Title to display" ),
43
+ default = _ ("Data export" ),
44
+ scope = Scope .settings
45
+ )
46
+ active_export_task_id = String (
47
+ # The UUID of the celery AsyncResult for the most recent export,
48
+ # IF we are sill waiting for it to finish
49
+ default = "" ,
50
+ scope = Scope .user_state ,
51
+ )
52
+ last_export_result = Dict (
53
+ # The info dict returned by the most recent successful export.
54
+ # If the export failed, it will have an "error" key set.
55
+ default = None ,
56
+ scope = Scope .user_state ,
57
+ )
58
+
59
+ has_author_view = True
60
+
61
+ def author_view (self , context = None ):
62
+ """ Studio View """
63
+ # Warn the user that this block will only work from the LMS. (Since the CMS uses
64
+ # different celery queues; our task listener is waiting for tasks on the LMS queue)
65
+ return Fragment ('<p>Data Export Block</p><p>This block only works from the LMS.</p>' )
66
+
67
+ def studio_view (self , context = None ):
68
+ """ View for editing Instructor Tool block in Studio. """
69
+ # Display friendly message explaining that the block is not editable.
70
+ return Fragment ('<p>This is a preconfigured block. It is not editable.</p>' )
71
+
72
+ def check_pending_export (self ):
73
+ """
74
+ If we're waiting for an export, see if it has finished, and if so, get the result.
75
+ """
76
+ from .tasks import export_data as export_data_task
77
+ if self .active_export_task_id :
78
+ async_result = export_data_task .AsyncResult (self .active_export_task_id )
79
+ if async_result .ready ():
80
+ self ._save_result (async_result )
81
+
82
+ def _save_result (self , task_result ):
83
+ """ Given an AsyncResult or EagerResult, save it. """
84
+ self .active_export_task_id = ''
85
+ if task_result .successful ():
86
+ self .last_export_result = task_result .result
87
+ else :
88
+ self .last_export_result = {'error' : str (task_result .result )}
89
+
90
+ def resource_string (self , path ):
91
+ """Handy helper for getting resources from our kit."""
92
+ data = pkg_resources .resource_string (__name__ , path )
93
+ return data .decode ("utf8" )
94
+
95
+ def student_view (self , context = None ):
96
+ """ Normal View """
97
+ if not self .user_is_staff ():
98
+ return Fragment ('<p>This interface can only be used by course staff.</p>' )
99
+
100
+ html = loader .render_django_template ('templates/export.html' )
101
+ frag = Fragment (html )
102
+ frag .add_javascript (self .resource_string ("static/js/src/export.js" ))
103
+ frag .initialize_js ('DataExportXBlock' )
104
+ return frag
105
+
106
+ @property
107
+ def download_url_for_last_report (self ):
108
+ """ Get the URL for the last report, if any """
109
+ # Unfortunately this is a bit inefficient due to the ReportStore API
110
+ if not self .last_export_result or 'error' in self .last_export_result :
111
+ return None
112
+ from lms .djangoapps .instructor_task .models import ReportStore
113
+ report_store = ReportStore .from_config (config_name = 'GRADES_DOWNLOAD' )
114
+ course_key = getattr (self .scope_ids .usage_id , 'course_key' , None )
115
+ return dict (report_store .links_for (course_key )).get (self .last_export_result ['report_filename' ])
116
+
117
+ def _get_status (self ):
118
+ self .check_pending_export ()
119
+ return {
120
+ 'export_pending' : bool (self .active_export_task_id ),
121
+ 'last_export_result' : self .last_export_result ,
122
+ 'download_url' : self .download_url_for_last_report ,
123
+ }
124
+
125
+ def raise_error (self , code , message ):
126
+ """
127
+ Raises an error and marks the block with a simulated failed task dict.
128
+ """
129
+ self .last_export_result = {
130
+ 'error' : message ,
131
+ }
132
+ self .display_data = None
133
+ raise JsonHandlerError (code , message )
134
+
135
+ @XBlock .json_handler
136
+ def get_status (self , data , suffix = '' ):
137
+ return self ._get_status ()
138
+
139
+ @XBlock .json_handler
140
+ def delete_export (self , data , suffix = '' ):
141
+ self ._delete_export ()
142
+ return self ._get_status ()
143
+
144
+ def _delete_export (self ):
145
+ self .last_export_result = None
146
+ self .display_data = None
147
+ self .active_export_task_id = ''
148
+
149
+ @XBlock .json_handler
150
+ def start_export (self , data , suffix = '' ):
151
+ """ Start a new asynchronous export """
152
+ if not self .user_is_staff ():
153
+ raise JsonHandlerError (403 , "Permission denied." )
154
+
155
+ # Launch task
156
+ from .tasks import export_data as export_data_task
157
+ self ._delete_export ()
158
+ # Make sure we nail down our state before sending off an asynchronous task.
159
+ self .save ()
160
+ async_result = export_data_task .delay (
161
+ # course_id not available in workbench.
162
+ str (getattr (self .runtime , 'course_id' , 'course_id' )),
163
+ )
164
+ if async_result .ready ():
165
+ # In development mode, the task may have executed synchronously.
166
+ # Store the result now, because we won't be able to retrieve it later :-/
167
+ if async_result .successful ():
168
+ # Make sure the result can be represented as JSON, since the non-eager celery
169
+ # requires that
170
+ json .dumps (async_result .result )
171
+ self ._save_result (async_result )
172
+ else :
173
+ # The task is running asynchronously. Store the result ID so we can query its progress:
174
+ self .active_export_task_id = async_result .id
175
+ return self ._get_status ()
176
+
177
+ @XBlock .json_handler
178
+ def cancel_export (self , request , suffix = '' ):
179
+ from .tasks import export_data as export_data_task
180
+ if self .active_export_task_id :
181
+ async_result = export_data_task .AsyncResult (self .active_export_task_id )
182
+ async_result .revoke ()
183
+ self ._delete_export ()
184
+
185
+ def _get_user_attr (self , attr ):
186
+ """Get an attribute of the current user."""
187
+ user_service = self .runtime .service (self , 'user' )
188
+ if user_service :
189
+ # May be None when creating bok choy test fixtures
190
+ return user_service .get_current_user ().opt_attrs .get (attr )
191
+ return None
192
+
193
+ def user_is_staff (self ):
194
+ """Return a Boolean value indicating whether the current user is a member of staff."""
195
+ return self ._get_user_attr ('edx-platform.user_is_staff' )
0 commit comments