-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
library_sourced_block.py
159 lines (140 loc) · 6.2 KB
/
library_sourced_block.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
"""
Library Sourced Content XBlock
"""
import logging
from copy import copy
from mako.template import Template as MakoTemplate
from xblock.core import XBlock
from xblock.fields import Scope, String, List
from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
from webob import Response
from web_fragments.fragment import Fragment
from xmodule.studio_editable import StudioEditableBlock as EditableChildrenMixin
from xmodule.validation import StudioValidation, StudioValidationMessage
log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
# Make '_' a no-op so we can scrape strings. Using lambda instead of
# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file
_ = lambda text: text
@XBlock.wants('library_tools') # Only needed in studio
class LibrarySourcedBlock(StudioEditableXBlockMixin, EditableChildrenMixin, XBlock):
"""
Library Sourced Content XBlock
Allows copying specific XBlocks from a Blockstore-based content library into
a modulestore-based course. The selected blocks are copied and become
children of this block.
When we implement support for Blockstore-based courses, it's expected we'll
use a different mechanism for importing library content into a course.
"""
display_name = String(
help=_("The display name for this component."),
default="Library Sourced Content",
display_name=_("Display Name"),
scope=Scope.content,
)
source_block_ids = List(
display_name=_("Library Blocks List"),
help=_("Enter the IDs of the library XBlocks that you wish to use."),
scope=Scope.content,
)
editable_fields = ("display_name", "source_block_ids")
has_children = True
has_author_view = True
resources_dir = 'assets/library_source_block'
MAX_BLOCKS_ALLOWED = 10
def __str__(self):
return f"LibrarySourcedBlock: {self.display_name}"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.source_block_ids:
self.has_children = False
def studio_view(self, context):
"""
Render a form for editing this XBlock
"""
fragment = Fragment()
static_content = ResourceLoader('common.djangoapps.pipeline_mako').load_unicode('templates/static_content.html')
render_react = MakoTemplate(static_content, default_filters=[]).get_def('renderReact')
react_content = render_react.render(
component="LibrarySourcedBlockPicker",
id="library-sourced-block-picker",
props={
'selectedXblocks': self.source_block_ids,
}
)
fragment.content = loader.render_django_template('templates/library-sourced-block-studio-view.html', {
'react_content': react_content,
'save_url': self.runtime.handler_url(self, 'submit_studio_edits'),
})
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/library_source_block.js'))
fragment.initialize_js('LibrarySourceBlockStudioView')
return fragment
def author_view(self, context):
"""
Renders the Studio preview view.
"""
fragment = Fragment()
context = {} if not context else copy(context) # Isolate context - without this there are weird bugs in Studio
# EditableChildrenMixin.render_children will render HTML that allows instructors to make edits to the children
context['can_move'] = False
self.render_children(context, fragment, can_reorder=False, can_add=False)
return fragment
def student_view(self, context):
"""
Renders the view that learners see.
"""
result = Fragment()
child_frags = self.runtime.render_children(self, context=context)
result.add_resources(child_frags)
result.add_content('<div class="library-sourced-content">')
for frag in child_frags:
result.add_content(frag.content)
result.add_content('</div>')
return result
def validate_field_data(self, validation, data):
"""
Validate this block's field data. Instead of checking fields like self.name, check the
fields set on data, e.g. data.name. This allows the same validation method to be re-used
for the studio editor.
"""
if len(data.source_block_ids) > self.MAX_BLOCKS_ALLOWED:
# Because importing library blocks is an expensive operation
validation.add(
ValidationMessage(
ValidationMessage.ERROR,
_("A maximum of {0} components may be added.").format(self.MAX_BLOCKS_ALLOWED)
)
)
def validate(self):
"""
Validates the state of this library_sourced_xblock Instance. This is the override of the general XBlock method,
and it will also ask its superclass to validate.
"""
validation = super().validate()
validation = StudioValidation.copy(validation)
if not self.source_block_ids:
validation.set_summary(
StudioValidationMessage(
StudioValidationMessage.NOT_CONFIGURED,
_("No XBlock has been configured for this component. Use the editor to select the target blocks."),
action_class='edit-button',
action_label=_("Open Editor")
)
)
return validation
@XBlock.handler
def submit_studio_edits(self, data, suffix=''):
"""
Save changes to this block, applying edits made in Studio.
"""
response = super().submit_studio_edits(data, suffix)
# Replace our current children with the latest ones from the libraries.
lib_tools = self.runtime.service(self, 'library_tools')
try:
lib_tools.import_from_blockstore(self, self.source_block_ids)
except Exception as err: # pylint: disable=broad-except
log.exception(err)
return Response(_("Importing Library Block failed - are the IDs valid and readable?"), status=400)
return response