Commit 125eeb30 by Alan Boudreault

first try of LightChild data persistence

parent 7ab8773d
...@@ -55,7 +55,7 @@ class AnswerBlock(LightChild): ...@@ -55,7 +55,7 @@ class AnswerBlock(LightChild):
@lazy @lazy
def student_input(self): def student_input(self):
""" """
Use lazy property instead of XBlock field, as __init__() doesn't support Use lazy property instead of XBlock field, as __init__() doesn't support
overwriting field values overwriting field values
""" """
# Only attempt to locate a model object for this block when the answer has a name # Only attempt to locate a model object for this block when the answer has a name
...@@ -79,7 +79,7 @@ class AnswerBlock(LightChild): ...@@ -79,7 +79,7 @@ class AnswerBlock(LightChild):
html = render_template('templates/html/answer_read_only.html', { html = render_template('templates/html/answer_read_only.html', {
'self': self, 'self': self,
}) })
fragment = Fragment(html) fragment = Fragment(html)
fragment.add_css_url(self.runtime.local_resource_url(self.xblock_container, 'public/css/answer.css')) fragment.add_css_url(self.runtime.local_resource_url(self.xblock_container, 'public/css/answer.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self.xblock_container, fragment.add_javascript_url(self.runtime.local_resource_url(self.xblock_container,
......
...@@ -56,7 +56,8 @@ class HTMLBlock(LightChild): ...@@ -56,7 +56,8 @@ class HTMLBlock(LightChild):
return block return block
def student_view(self, context=None): def student_view(self, context=None):
return Fragment(self.content) # HACK, TO MODIFY
return Fragment(self.content.get())
def mentoring_view(self, context=None): def mentoring_view(self, context=None):
return self.student_view(context) return self.student_view(context)
......
...@@ -24,6 +24,9 @@ ...@@ -24,6 +24,9 @@
# Imports ########################################################### # Imports ###########################################################
import logging import logging
import json
from lazy import lazy
from cStringIO import StringIO from cStringIO import StringIO
from lxml import etree from lxml import etree
...@@ -34,6 +37,8 @@ from xblock.core import XBlock ...@@ -34,6 +37,8 @@ from xblock.core import XBlock
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.plugin import Plugin from xblock.plugin import Plugin
from .models import LightChild as LightChildModel
try: try:
from xmodule_modifiers import replace_jump_to_id_urls from xmodule_modifiers import replace_jump_to_id_urls
except: except:
...@@ -194,6 +199,7 @@ class XBlockWithLightChildren(LightChildrenMixin, XBlock): ...@@ -194,6 +199,7 @@ class XBlockWithLightChildren(LightChildrenMixin, XBlock):
fragment = replace_jump_to_id_urls(course_id, jump_to_url, self, 'student_view', fragment, {}) fragment = replace_jump_to_id_urls(course_id, jump_to_url, self, 'student_view', fragment, {})
return fragment return fragment
class LightChild(Plugin, LightChildrenMixin): class LightChild(Plugin, LightChildrenMixin):
""" """
Base class for the light children Base class for the light children
...@@ -203,6 +209,31 @@ class LightChild(Plugin, LightChildrenMixin): ...@@ -203,6 +209,31 @@ class LightChild(Plugin, LightChildrenMixin):
def __init__(self, parent): def __init__(self, parent):
self.parent = parent self.parent = parent
self.xblock_container = parent.xblock_container self.xblock_container = parent.xblock_container
# This doesn't work, crash.... where should I trigger the lazy property to be loaded?
#self.load_student_data()
def __setattr__(self, name, value):
field = getattr(self, name) if hasattr(self, name) else None
# If the property is a LightChildField instance, use its setattr
if isinstance(field, LightChildField):
field.set(value)
else:
super(LightChild, self).__setattr__(name, value)
@lazy
def student_data(self):
"""
Use lazy property instead of XBlock field, as __init__() doesn't support
overwriting field values
"""
if not self.name:
return ''
student_data = self.get_lightchild_model_object().student_data
return student_data
@property @property
def runtime(self): def runtime(self):
...@@ -220,20 +251,96 @@ class LightChild(Plugin, LightChildrenMixin): ...@@ -220,20 +251,96 @@ class LightChild(Plugin, LightChildrenMixin):
xmodule_runtime = xmodule_runtime() xmodule_runtime = xmodule_runtime()
return xmodule_runtime return xmodule_runtime
def load_student_data(self):
"""
Load the values from the student_data in the database.
"""
student_data = self.student_data
fields = self.get_fields()
for field in fields:
if field in student_data:
setattr(self, field, student_data[field])
@classmethod
def get_fields(cls):
"""
Returns a list of all LightChildField of the class. Used for saving student data.
"""
return []
def save(self): def save(self):
pass """
Replicate data changes on the related Django model used for sharing of data accross XBlocks
"""
# Save all children
for child in self.get_children_objects():
child.save()
self.student_data = {}
# Get All LightChild fields to save
for field in self.get_fields():
self.student_data[field] = getattr(self, field).to_json()
if self.name:
lightchild_data = self.get_lightchild_model_object()
if lightchild_data.student_data != self.student_data:
lightchild_data.student_data = json.dumps(self.student_data)
lightchild_data.save()
def get_lightchild_model_object(self, name=None):
"""
Fetches the LightChild model object for the lightchild named `name`
"""
if not name:
name = self.name
if not name:
raise ValueError, 'LightChild.name field need to be set to a non-null/empty value'
student_id = self.xmodule_runtime.anonymous_student_id
course_id = self.xmodule_runtime.course_id
lightchild_data, created = LightChildModel.objects.get_or_create(
student_id=student_id,
course_id=course_id,
name=name,
)
return lightchild_data
class LightChildField(object): class LightChildField(object):
""" """
Fake field with no persistence - allows to keep XBlocks fields definitions on LightChild Fake field with no persistence - allows to keep XBlocks fields definitions on LightChild
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.value = kwargs.get('default', '') self.value = kwargs.get('default', '')
def __nonzero__(self): def __nonzero__(self):
return bool(self.value) return bool(self.value)
def set(self, value):
self.value = value
def get(self):
return self.value
def to_json(self):
"""
Returns the JSON representation of the LightChieldField.
"""
return self.value
def from_json(self, value):
"""
Returns value as a native full featured python type from a JSON value.
"""
pass
class String(LightChildField): class String(LightChildField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
...@@ -259,11 +366,19 @@ class Integer(LightChildField): ...@@ -259,11 +366,19 @@ class Integer(LightChildField):
def __nonzero__(self): def __nonzero__(self):
try: try:
int(self.value) int(self.value)
except TypeError, ValueError: # not an integer except (TypeError, ValueError): # not an integer
return False return False
return self.value is not None return self.value is not None
def set(self, value):
self.value = int(value)
def from_json(self, value):
if value is None or value == '':
return None
return int(value)
class Boolean(LightChildField): class Boolean(LightChildField):
pass pass
......
...@@ -43,4 +43,4 @@ class Migration(SchemaMigration): ...@@ -43,4 +43,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['mentoring'] complete_apps = ['mentoring']
\ No newline at end of file
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'LightChild'
db.create_table('mentoring_lightchild', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
('student_id', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
('student_data', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
('created_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('modified_on', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
))
db.send_create_signal('mentoring', ['LightChild'])
# Adding unique constraint on 'LightChild', fields ['student_id', 'course_id', 'name']
db.create_unique('mentoring_lightchild', ['student_id', 'course_id', 'name'])
def backwards(self, orm):
# Removing unique constraint on 'LightChild', fields ['student_id', 'course_id', 'name']
db.delete_unique('mentoring_lightchild', ['student_id', 'course_id', 'name'])
# Deleting model 'LightChild'
db.delete_table('mentoring_lightchild')
models = {
'mentoring.answer': {
'Meta': {'unique_together': "(('student_id', 'course_id', 'name'),)", 'object_name': 'Answer'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'student_input': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
},
'mentoring.lightchild': {
'Meta': {'unique_together': "(('student_id', 'course_id', 'name'),)", 'object_name': 'LightChild'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'student_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
}
}
complete_apps = ['mentoring']
...@@ -49,3 +49,21 @@ class Answer(models.Model): ...@@ -49,3 +49,21 @@ class Answer(models.Model):
# Force validation of max_length # Force validation of max_length
self.full_clean() self.full_clean()
super(Answer, self).save(*args, **kwargs) super(Answer, self).save(*args, **kwargs)
class LightChild(models.Model):
"""
Django model used to store LightChild student data that need to be shared and queried accross
XBlock instances (workaround). Since this is temporary, `data` are stored in json.
"""
class Meta:
app_label = 'mentoring'
unique_together = (('student_id', 'course_id', 'name'),)
name = models.CharField(max_length=50, db_index=True)
student_id = models.CharField(max_length=32, db_index=True)
course_id = models.CharField(max_length=50, db_index=True)
student_data = models.TextField(blank=True, default='')
created_on = models.DateTimeField('created on', auto_now_add=True)
modified_on = models.DateTimeField('modified on', auto_now=True)
...@@ -46,6 +46,12 @@ class MRQBlock(QuestionnaireAbstractBlock): ...@@ -46,6 +46,12 @@ class MRQBlock(QuestionnaireAbstractBlock):
max_attempts = Integer(help="Number of max attempts for this questions", default=None, scope=Scope.content) max_attempts = Integer(help="Number of max attempts for this questions", default=None, scope=Scope.content)
num_attempts = Integer(help="Number of attempts a user has answered for this questions", scope=Scope.user_state) num_attempts = Integer(help="Number of attempts a user has answered for this questions", scope=Scope.user_state)
@classmethod
def get_fields(cls):
return [
'num_attempts'
]
def submit(self, submissions): def submit(self, submissions):
log.debug(u'Received MRQ submissions: "%s"', submissions) log.debug(u'Received MRQ submissions: "%s"', submissions)
...@@ -66,7 +72,7 @@ class MRQBlock(QuestionnaireAbstractBlock): ...@@ -66,7 +72,7 @@ class MRQBlock(QuestionnaireAbstractBlock):
completed = completed and choice_completed completed = completed and choice_completed
results.append({ results.append({
'value': choice.value, 'value': choice.value.get(),
'selected': choice_selected, 'selected': choice_selected,
'completed': choice_completed, 'completed': choice_completed,
'tips': render_template('templates/html/tip_choice_group.html', { 'tips': render_template('templates/html/tip_choice_group.html', {
...@@ -80,12 +86,13 @@ class MRQBlock(QuestionnaireAbstractBlock): ...@@ -80,12 +86,13 @@ class MRQBlock(QuestionnaireAbstractBlock):
# What's the proper way to get my value saved? it doesn't work without '.value' # What's the proper way to get my value saved? it doesn't work without '.value'
# this is incorrect and the num_attempts is resetted if we restart the server. # this is incorrect and the num_attempts is resetted if we restart the server.
self.num_attempts.value = int(self.num_attempts) + 1 if self.max_attempts else 0 num_attempts = self.num_attempts.get() + 1 if self.max_attempts else 0
setattr(self, 'num_attempts', num_attempts)
max_attempts_reached = False max_attempts_reached = False
if self.max_attempts: if self.max_attempts:
max_attempts = int(self.max_attempts) max_attempts = self.max_attempts.get()
num_attempts = int(self.num_attempts) num_attempts = self.num_attempts.get()
max_attempts_reached = num_attempts >= max_attempts max_attempts_reached = num_attempts >= max_attempts
if max_attempts_reached and (not completed or num_attempts > max_attempts): if max_attempts_reached and (not completed or num_attempts > max_attempts):
...@@ -96,12 +103,13 @@ class MRQBlock(QuestionnaireAbstractBlock): ...@@ -96,12 +103,13 @@ class MRQBlock(QuestionnaireAbstractBlock):
self.student_choices = submissions self.student_choices = submissions
result = { result = {
'submissions': submissions, 'submissions': submissions,
'completed': completed, 'completed': completed,
'choices': results, 'choices': results,
'message': self.message, 'message': self.message.get(),
'max_attempts': int(self.max_attempts) if self.max_attempts else None, 'max_attempts': self.max_attempts.get() if self.max_attempts else None,
'num_attempts': int(self.num_attempts) 'num_attempts': self.num_attempts.get()
} }
log.debug(u'MRQ submissions result: %s', result) log.debug(u'MRQ submissions result: %s', result)
return result return result
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment