| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 |
|
|---|
| 13 |
|
|---|
| 14 |
|
|---|
| 15 |
|
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 |
|
|---|
| 19 |
from AccessControl import ClassSecurityInfo, getSecurityManager |
|---|
| 20 |
from Products.Archetypes.public import * |
|---|
| 21 |
from Products.CMFCore.utils import getToolByName |
|---|
| 22 |
from config import PROJECTNAME, MODIFY_CONTENT, VIEW, to_unicode |
|---|
| 23 |
from FieldsWidgets import ChapterField, ExerciseWidget |
|---|
| 24 |
from Schemata import no_description, material_schema, community_editing_schema, draft_schema |
|---|
| 25 |
from Material import Material |
|---|
| 26 |
from permissions import MODIFY_CONTENT |
|---|
| 27 |
from random import shuffle |
|---|
| 28 |
from Products.LeMill import LeMillMessageFactory as _ |
|---|
| 29 |
from Products.MailHost.MailHost import MailHostError |
|---|
| 30 |
import re |
|---|
| 31 |
|
|---|
| 32 |
exercise_schema = Schema(( |
|---|
| 33 |
ChapterField('bodyText', |
|---|
| 34 |
accessor="getBodyText", |
|---|
| 35 |
edit_accessor = 'getRawBodyText', |
|---|
| 36 |
mutator = "setBodyText", |
|---|
| 37 |
index='ZCTextIndex', |
|---|
| 38 |
index_method = 'getOnlyText', |
|---|
| 39 |
copied_in_translation=True, |
|---|
| 40 |
searchable = True, |
|---|
| 41 |
deleteEmptyChapters = False, |
|---|
| 42 |
allowable_content_types = ['text/html',], |
|---|
| 43 |
allow_file_upload = False, |
|---|
| 44 |
default_output_type = 'text/x-html-captioned', |
|---|
| 45 |
default_content_type = 'text/html', |
|---|
| 46 |
default=[{'type':'text_block', 'text':''}], |
|---|
| 47 |
widget=ExerciseWidget(label = "Body text", |
|---|
| 48 |
label_msgid = "label_bodytext", |
|---|
| 49 |
i18n_domain = "lemill", |
|---|
| 50 |
), |
|---|
| 51 |
), |
|---|
| 52 |
)) |
|---|
| 53 |
|
|---|
| 54 |
schema = material_schema + community_editing_schema + no_description + draft_schema + exercise_schema |
|---|
| 55 |
schema = schema.copy() |
|---|
| 56 |
schema.moveField('rights', pos='bottom') |
|---|
| 57 |
schema.moveField('language', after='bodyText') |
|---|
| 58 |
schema['title'].required = True |
|---|
| 59 |
schema.moveField('hideDrafts', before='rights') |
|---|
| 60 |
|
|---|
| 61 |
blanks= re.compile("(\{.*?\})") |
|---|
| 62 |
|
|---|
| 63 |
class ExerciseMaterial(Material): |
|---|
| 64 |
"""Exercise page""" |
|---|
| 65 |
schema = schema |
|---|
| 66 |
meta_type = "ExerciseMaterial" |
|---|
| 67 |
archetype_name = "ExerciseMaterial" |
|---|
| 68 |
default_location = 'content/exercises' |
|---|
| 69 |
security = ClassSecurityInfo() |
|---|
| 70 |
security.declareObjectPublic() |
|---|
| 71 |
aliases = { |
|---|
| 72 |
'(Default)' : 'fullscreen_view', |
|---|
| 73 |
'view' : 'base_view', |
|---|
| 74 |
'edit' : 'base_edit', |
|---|
| 75 |
'feedback' : 'exercise_feedback', |
|---|
| 76 |
} |
|---|
| 77 |
|
|---|
| 78 |
def separateBlanksFromText(self, text): |
|---|
| 79 |
pieces=[] |
|---|
| 80 |
answers=[] |
|---|
| 81 |
previous_was_blank=False |
|---|
| 82 |
for piece in re.split(blanks, text): |
|---|
| 83 |
if piece.startswith('{'): |
|---|
| 84 |
answer=to_unicode(piece.strip('{} ')) |
|---|
| 85 |
if previous_was_blank: |
|---|
| 86 |
answers[-1].append(answer) |
|---|
| 87 |
else: |
|---|
| 88 |
answers.append([answer]) |
|---|
| 89 |
previous_was_blank=True |
|---|
| 90 |
elif piece=='': |
|---|
| 91 |
pass |
|---|
| 92 |
else: |
|---|
| 93 |
pieces.append(to_unicode(piece)) |
|---|
| 94 |
previous_was_blank=False |
|---|
| 95 |
return pieces, answers |
|---|
| 96 |
|
|---|
| 97 |
def replaceBlanksWithInputTag(self, text, index, readonly=False, give_answers=False): |
|---|
| 98 |
pieces, answers=self.separateBlanksFromText(text) |
|---|
| 99 |
inputs=[] |
|---|
| 100 |
results=[] |
|---|
| 101 |
if readonly: |
|---|
| 102 |
readonly=' readonly="1"' |
|---|
| 103 |
else: |
|---|
| 104 |
readonly='' |
|---|
| 105 |
for i, answer in enumerate(answers): |
|---|
| 106 |
if give_answers: |
|---|
| 107 |
answer=''.join('{%s}' % a for a in answer) |
|---|
| 108 |
else: |
|---|
| 109 |
answer="" |
|---|
| 110 |
a=u"""<input type="text" value="%s"%s name="exercise_%s_answer_%s" id="exercise_%s_answer_%s" />""" % (to_unicode(answer), readonly, index, i, index, i) |
|---|
| 111 |
inputs.append(a) |
|---|
| 112 |
for piece in pieces: |
|---|
| 113 |
results.append(piece) |
|---|
| 114 |
if inputs: |
|---|
| 115 |
results.append(inputs.pop(0)) |
|---|
| 116 |
if inputs: |
|---|
| 117 |
results+=inputs |
|---|
| 118 |
return u''.join(results) |
|---|
| 119 |
|
|---|
| 120 |
def checkFillInTheBlankAnswersFromForm(self, form, text, index): |
|---|
| 121 |
""" Checks fill-in-the-blanks exercise from web form, |
|---|
| 122 |
returns completed exercise text and lists of correct and wrong answers """ |
|---|
| 123 |
pieces, answers=self.separateBlanksFromText(text) |
|---|
| 124 |
corrects=wrongs=0 |
|---|
| 125 |
corrected_answers=[] |
|---|
| 126 |
for i, choices in enumerate(answers): |
|---|
| 127 |
a=to_unicode(form.get('exercise_%s_answer_%s' % (index, i), '')) |
|---|
| 128 |
if a in choices: |
|---|
| 129 |
corrects+=1 |
|---|
| 130 |
corrected_answers.append('<span class="correct_fitb">%s</span>' % a) |
|---|
| 131 |
else: |
|---|
| 132 |
wrongs+=1 |
|---|
| 133 |
corrected_answers.append('<span class="incorrect_fitb">%s</span>' % a) |
|---|
| 134 |
corrected_answers.append('<span class="corrected_fitb">%s</span>' % '/'.join(choices)) |
|---|
| 135 |
results=[] |
|---|
| 136 |
for piece in pieces: |
|---|
| 137 |
results.append(piece) |
|---|
| 138 |
if answers and corrected_answers: |
|---|
| 139 |
results.append(corrected_answers.pop(0)) |
|---|
| 140 |
results.append('\n') |
|---|
| 141 |
return u''.join(results), corrects, wrongs |
|---|
| 142 |
|
|---|
| 143 |
def checkMultipleChoiceFromForm(self, form, question, choices, index): |
|---|
| 144 |
""" Checks form for multiple choice test, returns exercise with correct answers marked and number of right and wrong answers """ |
|---|
| 145 |
corrects=wrongs=0 |
|---|
| 146 |
|
|---|
| 147 |
cor=0 |
|---|
| 148 |
multiple=False |
|---|
| 149 |
for q,c in choices: |
|---|
| 150 |
if c: |
|---|
| 151 |
cor+=1 |
|---|
| 152 |
if cor!=1: |
|---|
| 153 |
multiple=True |
|---|
| 154 |
results=['<p><strong>%s</strong></p>\n' % to_unicode(question)] |
|---|
| 155 |
|
|---|
| 156 |
if multiple: |
|---|
| 157 |
for i, choice in enumerate(choices): |
|---|
| 158 |
checked=form.get('exercise_%s_checkbox_%s' % (index, i), '') |
|---|
| 159 |
correct=(checked and choice[1]) or not (checked or choice[1]) |
|---|
| 160 |
checked_str='' |
|---|
| 161 |
if checked: checked_str=' checked="checked"' |
|---|
| 162 |
if correct: |
|---|
| 163 |
style='correct_mc' |
|---|
| 164 |
corrects+=1 |
|---|
| 165 |
else: |
|---|
| 166 |
style='incorrect_mc' |
|---|
| 167 |
wrongs+=1 |
|---|
| 168 |
results.append('<div><span class="%s"><input type="checkbox" disabled="disabled" %s />%s</span></div>\n' % (style, checked_str, to_unicode(choice[0]))) |
|---|
| 169 |
else: |
|---|
| 170 |
checked=int(form.get('exercise_%s_radio' % index, '-1')) |
|---|
| 171 |
for i, choice in enumerate(choices): |
|---|
| 172 |
style='' |
|---|
| 173 |
if checked==i: |
|---|
| 174 |
checked_str=' checked="checked"' |
|---|
| 175 |
if choice[1]: |
|---|
| 176 |
style='correct_mc' |
|---|
| 177 |
corrects+=1 |
|---|
| 178 |
else: |
|---|
| 179 |
style='incorrect_mc' |
|---|
| 180 |
else: |
|---|
| 181 |
checked_str='' |
|---|
| 182 |
if choice[1]: |
|---|
| 183 |
style='incorrect_mc' |
|---|
| 184 |
wrongs+=1 |
|---|
| 185 |
results.append('<div><span class="%s"><input type="radio" name="exercise_%s_radio" disabled="disabled" %s />%s</span></div>\n' % (style, index, checked_str, to_unicode(choice[0]))) |
|---|
| 186 |
return u''.join(results), corrects, wrongs |
|---|
| 187 |
|
|---|
| 188 |
|
|---|
| 189 |
def checkOpenEndedFromForm(self, form, question, index): |
|---|
| 190 |
""" Reports back the given answer """ |
|---|
| 191 |
results=[question,'\n\n'] |
|---|
| 192 |
a=form.get('exercise_%s_answer' % index, '') |
|---|
| 193 |
results.append(a) |
|---|
| 194 |
results.append('\n\n') |
|---|
| 195 |
return u''.join(results) |
|---|
| 196 |
|
|---|
| 197 |
|
|---|
| 198 |
def checkExercise(self): |
|---|
| 199 |
""" Builds a dictionary of required info for web feedback form """ |
|---|
| 200 |
lt=getToolByName(self, 'lemill_tool') |
|---|
| 201 |
form=self.REQUEST.form |
|---|
| 202 |
REQUEST=self.REQUEST |
|---|
| 203 |
students_name = to_unicode(REQUEST.get('your_name','')) |
|---|
| 204 |
students_email= REQUEST.get('students_email','') |
|---|
| 205 |
teachers_email = REQUEST.get('teachers_email','') |
|---|
| 206 |
captcha=self.lemill_tool.validateCaptcha(REQUEST.get('recaptcha_challenge_field',''), REQUEST.get('recaptcha_response_field','')) |
|---|
| 207 |
exercise_title=self.Title() |
|---|
| 208 |
exercise_url=self.absolute_url() |
|---|
| 209 |
chapters=[] |
|---|
| 210 |
total=0 |
|---|
| 211 |
max_total=0 |
|---|
| 212 |
for index, chapter in enumerate(self.getBodyText()): |
|---|
| 213 |
text=chapter['text'] |
|---|
| 214 |
ctype=chapter['type'] |
|---|
| 215 |
if ctype=='fill_in_the_blanks': |
|---|
| 216 |
s, cs, ws=self.checkFillInTheBlankAnswersFromForm(form, text,index) |
|---|
| 217 |
max_score=chapter.get('max_points',3.0) |
|---|
| 218 |
score=(cs/(cs+ws or 1))*max_score |
|---|
| 219 |
total+=score |
|---|
| 220 |
max_total+=max_score |
|---|
| 221 |
chapters.append({'type':ctype, 'score':score, 'max_score':max_score,'text':s}) |
|---|
| 222 |
elif chapter['type']=='open_ended': |
|---|
| 223 |
s=self.checkOpenEndedFromForm(form, text, index) |
|---|
| 224 |
chapters.append({'type':ctype, 'text':s, 'max_score':0}) |
|---|
| 225 |
elif chapter['type']=='multiple_choices': |
|---|
| 226 |
s, cs, ws=self.checkMultipleChoiceFromForm(form, text, chapter['choices'], index) |
|---|
| 227 |
max_score=chapter.get('max_points',3.0) |
|---|
| 228 |
score=(cs/(cs+ws or 1))*max_score |
|---|
| 229 |
total+=score |
|---|
| 230 |
max_total+=max_score |
|---|
| 231 |
chapters.append({'type':ctype, 'score':score,'max_score':max_score,'text':s}) |
|---|
| 232 |
dict={'chapters':chapters, 'title':exercise_title,'url':exercise_url,'max_total':max_total, 'total':total, 'percent':(total/(max_total or 1))*100.0, 'teachers_email':teachers_email,'students_email':students_email,'students_name':students_name} |
|---|
| 233 |
return dict |
|---|
| 234 |
|
|---|
| 235 |
|
|---|
| 236 |
def sendAnswers(self, REQUEST): |
|---|
| 237 |
""" Send e-mail to a teacher """ |
|---|
| 238 |
captcha_c=REQUEST.get('recaptcha_challenge_field','') |
|---|
| 239 |
captcha_r=REQUEST.get('recaptcha_response_field','') |
|---|
| 240 |
captcha=self.lemill_tool.validateCaptcha(captcha_c, captcha_r) |
|---|
| 241 |
students_email= REQUEST.get('students_email','') |
|---|
| 242 |
|
|---|
| 243 |
if students_email and not captcha: |
|---|
| 244 |
|
|---|
| 245 |
|
|---|
| 246 |
|
|---|
| 247 |
return REQUEST.RESPONSE |
|---|
| 248 |
body=self.exercise_feedback() |
|---|
| 249 |
if students_email and captcha: |
|---|
| 250 |
msg,msg_type=self._sendAnswers(REQUEST, body) |
|---|
| 251 |
lt=getToolByName(self, 'lemill_tool') |
|---|
| 252 |
lt.addPortalMessage(msg,msg_type) |
|---|
| 253 |
return body |
|---|
| 254 |
|
|---|
| 255 |
def _sendAnswers(self, REQUEST, body): |
|---|
| 256 |
|
|---|
| 257 |
|
|---|
| 258 |
|
|---|
| 259 |
|
|---|
| 260 |
mhost=self.MailHost |
|---|
| 261 |
students_name = to_unicode(REQUEST.get('your_name','')) |
|---|
| 262 |
|
|---|
| 263 |
students_email= REQUEST.get('students_email','') |
|---|
| 264 |
teachers_email = REQUEST.get('teachers_email','') |
|---|
| 265 |
d={'name':students_name, |
|---|
| 266 |
'exercise_title':to_unicode(self.Title()), |
|---|
| 267 |
'exercise_url':to_unicode(self.absolute_url())} |
|---|
| 268 |
if not students_email or (teachers_email and not students_name): |
|---|
| 269 |
msg = self.translate("text_mail_feedback_no_information", "You have not provided enough information to send an e-mail.",domain='lemill') |
|---|
| 270 |
return (msg, 'warn') |
|---|
| 271 |
|
|---|
| 272 |
|
|---|
| 273 |
if students_name: |
|---|
| 274 |
msubject=self.translate(u"LeMill exercise '%(exercise_title)s' by %(name)s", domain='lemill') % d |
|---|
| 275 |
else: |
|---|
| 276 |
msubject=self.translate(u"LeMill exercise '%(exercise_title)s' feedback", domain='lemill') % d |
|---|
| 277 |
if teachers_email: |
|---|
| 278 |
mto=teachers_email |
|---|
| 279 |
mcc=students_email |
|---|
| 280 |
else: |
|---|
| 281 |
mto=students_email |
|---|
| 282 |
mcc='' |
|---|
| 283 |
|
|---|
| 284 |
try: |
|---|
| 285 |
|
|---|
| 286 |
|
|---|
| 287 |
mhost.secureSend(body, mto, "LeMill <no-reply@lemill.net>", subject=msubject, mcc=mcc, subtype="html", charset='utf-8') |
|---|
| 288 |
msg = self.translate("text_mail_feedback_message_sent", "The e-mail has been sent.",domain='lemill') |
|---|
| 289 |
|
|---|
| 290 |
return (msg, 'mess') |
|---|
| 291 |
except (MailHostError, AttributeError): |
|---|
| 292 |
msg = self.translate("The e-mail could not be sent.",domain='lemill') |
|---|
| 293 |
return (msg, 'warn') |
|---|
| 294 |
|
|---|
| 295 |
def prepareForPDF(self): |
|---|
| 296 |
""" Replaces form fields with dotted lines """ |
|---|
| 297 |
text=Material.prepareForPDF(self) |
|---|
| 298 |
|
|---|
| 299 |
submit_pattern = re.compile('<fieldset class="visualNoPrint">.*?</fieldset>', re.MULTILINE | re.DOTALL | re.IGNORECASE) |
|---|
| 300 |
text = re.sub(submit_pattern, '', text) |
|---|
| 301 |
|
|---|
| 302 |
input_text_pattern = re.compile('<input type="text" value="" name="exercise_.*?/>',re.DOTALL | re.IGNORECASE | re.MULTILINE) |
|---|
| 303 |
text = re.sub(input_text_pattern, '.......... ', text) |
|---|
| 304 |
|
|---|
| 305 |
textarea_pattern = re.compile('<textarea.*?</textarea>', re.DOTALL | re.IGNORECASE | re.MULTILINE) |
|---|
| 306 |
text = re.sub(textarea_pattern, '<br />'.join(('.'*80, '.'*80)), text) |
|---|
| 307 |
return text |
|---|
| 308 |
|
|---|
| 309 |
def fixBrokenChoice(self): |
|---|
| 310 |
""" If choice -type is mapped wrong, fix it """ |
|---|
| 311 |
old_values=self.getRawBodyText() |
|---|
| 312 |
new_values=[] |
|---|
| 313 |
fixed=False |
|---|
| 314 |
for vald in old_values: |
|---|
| 315 |
if vald['type']=='choice' and isinstance(vald['text'], list): |
|---|
| 316 |
|
|---|
| 317 |
broken_text=vald['text'] |
|---|
| 318 |
vald['text']=broken_text[0] |
|---|
| 319 |
choices=[(broken_text[1][0], 1)] |
|---|
| 320 |
for item in broken_text[2]: |
|---|
| 321 |
choices.append((item, 0)) |
|---|
| 322 |
vald['choices']=choices |
|---|
| 323 |
vald['type']='multiple_choices' |
|---|
| 324 |
fixed=True |
|---|
| 325 |
new_values.append(vald) |
|---|
| 326 |
if fixed: |
|---|
| 327 |
self.setBodyText(new_values) |
|---|
| 328 |
|
|---|
| 329 |
|
|---|
| 330 |
registerType(ExerciseMaterial, PROJECTNAME) |
|---|