root/trunk/ExerciseMaterial.py

Revision 3208, 14.3 kB (checked in by jukka, 2 days ago)

Fixed #2049

Line 
1 # Copyright 2006 by the LeMill Team (see AUTHORS)
2 #
3 # This file is part of LeMill.
4 #
5 # LeMill is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # LeMill is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with LeMill; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1
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: # leftovers
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         # Check if this is a choice or multiple choice form
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             #msg = self.translate("Invalid answer for humanity test.",domain='lemill')
245             #lt=getToolByName(self, 'lemill_tool')
246             #lt.addPortalMessage(msg, 'warn')
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         ### Note for future unicode problems: self.translate returns unicode objects,
257         # but forms return utf-8 encoded strings. joining them together or embedding them with %s is harmful.
258         # when building longer strings, they should be manipulated as unicode (convert utf-8 strings with to_unicode()), but when
259         # sending email, the text should be utf-8 encoded str.   
260         mhost=self.MailHost
261         students_name = to_unicode(REQUEST.get('your_name',''))
262         #students_email = to_unicode(REQUEST.get('students_email',''))
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         # Build message body
272         # Other email fields
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         # Send
284         try:
285             # if the body is page it should already be utf-8?
286             #body=body.encode('utf-8')
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         # Getting rid of submit for exercises
299         submit_pattern = re.compile('<fieldset class="visualNoPrint">.*?</fieldset>', re.MULTILINE | re.DOTALL | re.IGNORECASE)
300         text = re.sub(submit_pattern, '', text)
301         # Replace text inputs with some dots
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         # Replace textarea with some lines of dots
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                  # [text, [correct_answers], [wrong_answers]]
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)
Note: See TracBrowser for help on using the browser.