Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to ask for numerical value on drop. #1

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 8 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,32 +93,22 @@ You can define an arbitrary number of drag items.
Testing
-------

1. In a virtualenv, run
In a virtualenv, run

```bash
$ (cd .../xblock-sdk/; pip install -r requirements.txt)
$ (cd .../xblock-drag-and-drop-v2/; pip install -r tests/requirements.txt)
$ cd .../xblock-drag-and-drop-v2/
$ pip install -r tests/requirements.txt
```

2. In the xblock-sdk repository, create the following configuration
file in `workbench/settings_drag_and_drop_v2.py`

```python
from settings import *

INSTALLED_APPS += ('drag_and_drop_v2',)
DATABASES['default']['NAME'] = 'workbench.db'
```

3. Run this to sync the database before starting the workbench
(answering no to the superuser question is ok):
To run the tests, from the xblock-drag-and-drop-v2 repository root:

```bash
$ ../xblock-sdk/manage.py syncdb --settings=workbench.settings_drag_and_drop_v2
$ tests/manage.py test --rednose
```

4. To run the tests, from the xblock-drag-and-drop-v2 repository root:
To include coverage report (although selenium tends to crash with
segmentation faults when collection test coverage):

```bash
$ DJANGO_SETTINGS_MODULE="workbench.settings_drag_and_drop_v2" nosetests --rednose --verbose --with-cover --cover-package=drag_and_drop_v2 --with-django
$ tests/manage.py test --rednose --with-cover --cover-package=drag_and_drop_v2
```
158 changes: 130 additions & 28 deletions drag_and_drop_v2/drag_and_drop_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,18 @@ def get_data(self, request, suffix=''):
# Strip answers
del item['feedback']
del item['zone']
item['inputOptions'] = item.has_key('inputOptions')

if not self._is_finished():
del data['feedback']['finish']

item_state = self._get_item_state()
for item_id, item in item_state.iteritems():
definition = next(i for i in self.data['items'] if str(i['id']) == item_id)
item['correct_input'] = self._is_correct_input(definition, item.get('input'))

data['state'] = {
'items': self.item_state,
'items': item_state,
'finished': self._is_finished()
}

Expand All @@ -171,55 +177,149 @@ def do_attempt(self, attempt, suffix=''):
item = next(i for i in self.data['items'] if i['id'] == attempt['val'])
tot_items = sum(1 for i in self.data['items'] if i['zone'] != 'none')

state = None
feedback = item['feedback']['incorrect']
final_feedback = None
is_correct = False

if item['zone'] == attempt['zone']:
self.item_state[item['id']] = (attempt['top'], attempt['left'])

is_correct = True

is_correct_location = False

if 'input' in attempt:
state = self._get_item_state().get(str(item['id']))
if state:
state['input'] = attempt['input']
is_correct_location = True
if self._is_correct_input(item, attempt['input']):
is_correct = True
feedback = item['feedback']['correct']
else:
is_correct = False
elif item['zone'] == attempt['zone']:
is_correct_location = True
if item.has_key('inputOptions'):
# Input value will have to be provided for the item.
# It is not (yet) correct and no feedback should be shown yet.
is_correct = False
feedback = None
else:
# If this item has no input value set, we are done with it.
is_correct = True
feedback = item['feedback']['correct']
state = {'top': attempt['top'], 'left': attempt['left']}

if state:
self.item_state[str(item['id'])] = state

if self._is_finished():
final_feedback = self.data['feedback']['finish']

# don't publish the grade if the student has already completed the exercise
if not self.completed:
if self._is_finished():
final_feedback = self.data['feedback']['finish']

# don't publish the grade if the student has already completed the exercise
if not self.completed:
if self._is_finished():
self.completed = True
try:
self.runtime.publish(self, 'grade', {
'value': len(self.item_state) / float(tot_items) * self.weight,
'max_value': self.weight,
})
except NotImplementedError:
# Note, this publish method is unimplemented in Studio runtimes,
# so we have to figure that we're running in Studio for now
pass
self.completed = True
try:
self.runtime.publish(self, 'grade', {
'value': self._get_grade(),
'max_value': self.weight,
})
except NotImplementedError:
# Note, this publish method is unimplemented in Studio runtimes,
# so we have to figure that we're running in Studio for now
pass

self.runtime.publish(self, 'xblock.drag-and-drop-v2.item.dropped', {
'user_id': self.scope_ids.user_id,
'component_id': self._get_unique_id(),
'item_id': item['id'],
'location': attempt['zone'],
'location': attempt.get('zone'),
'input': attempt.get('input'),
'is_correct_location': is_correct_location,
'is_correct': is_correct,
})

return {
'correct': is_correct,
'correct_location': is_correct_location,
'finished': self._is_finished(),
'final_feedback': final_feedback,
'feedback': item['feedback']['correct'] if is_correct else item['feedback']['incorrect']
'feedback': feedback
}

@XBlock.json_handler
def reset(self, data, suffix=''):
self.item_state = {}
return {'result':'success'}

def _get_item_state(self):
"""
Returns the user item state.
Converts to a dict if data is stored in legacy tuple form.
"""
state = {}

for item_id, item in self.item_state.iteritems():
if isinstance(item, dict):
state[item_id] = item
else:
state[item_id] = {'top': item[0], 'left': item[1]}

return state

def _is_correct_input(self, item, val):
"""
Is submitted numerical value within the tolerated margin for this item.
"""
input_options = item.get('inputOptions')

if input_options:
try:
submitted_value = float(val)
except:
return False
else:
expected_value = input_options['value']
margin = input_options['margin']
return abs(submitted_value - expected_value) <= margin
else:
return True

def _get_grade(self):
"""
Returns the student's grade for this block.
"""
correct_count = 0
total_count = 0
item_state = self._get_item_state()

for item in self.data['items']:
if item['zone'] != 'none':
total_count += 1
item_id = str(item['id'])
if item_id in item_state:
if self._is_correct_input(item, item_state[item_id].get('input')):
correct_count += 1

return correct_count / float(total_count) * self.weight

def _is_finished(self):
"""All items are at their correct place"""
tot_items = sum(1 for i in self.data['items'] if i['zone'] != 'none')
return len(self.item_state) == tot_items
"""
All items are at their correct place and a value has been
submitted for each item that expects a value.
"""
completed_count = 0
total_count = 0
item_state = self._get_item_state()
for item in self.data['items']:
if item['zone'] != 'none':
total_count += 1
item_id = str(item['id'])
if item_id in item_state:
if item.has_key('inputOptions'):
if item_state[item_id].has_key('input'):
completed_count += 1
else:
completed_count += 1

return completed_count == total_count

@XBlock.json_handler
def publish_event(self, data, suffix=''):
Expand All @@ -244,5 +344,7 @@ def _get_unique_id(self):

@staticmethod
def workbench_scenarios():
"""A canned scenario for display in the workbench."""
"""
A canned scenario for display in the workbench.
"""
return [("Drag-and-drop-v2 scenario", "<vertical_demo><drag-and-drop-v2/></vertical_demo>")]
45 changes: 44 additions & 1 deletion drag_and_drop_v2/public/css/drag_and_drop.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,56 @@
z-index: 10 !important;
margin-bottom: 5px;
padding: 10px;
opacity: 1;
}

.xblock--drag-and-drop .drag-container .items .option img {
max-width: 100%;
}

.xblock--drag-and-drop .option.fade { opacity: 0.6; }
.xblock--drag-and-drop .drag-container .items .option .numerical-input {
display: none;
height: 32px;
position: absolute;
left: calc(100% + 5px);
top: calc(50% - 16px);
}

.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input {
display: block;
}

.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input .input {
display: inline-block;
width: 144px;
}

.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input .submit-input {
box-sizing: border-box;
position: absolute;
left: 150px;
top: 4px;
height: 24px;
}

.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input.correct .input-submit,
.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input.incorrect .input-submit {
display: none;
}

.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input.correct .input {
background: #ceffce;
color: #0dad0d;
}

.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input.incorrect .input {
background: #ffcece;
color: #ad0d0d;
}

.xblock--drag-and-drop .drag-container .items .option.fade {
opacity: 0.5;
}


/*** Drop Target ***/
Expand Down
5 changes: 5 additions & 0 deletions drag_and_drop_v2/public/css/drag_and_drop_edit.css
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@
width: 40px;
}

.xblock--drag-and-drop .items-form .item-numerical-value,
.xblock--drag-and-drop .items-form .item-numerical-margin {
width: 60px;
}

.xblock--drag-and-drop .items-form textarea {
width: 97%;
margin: 0 1%;
Expand Down
Loading