source: trunk/ExerciseMaterial.py @ 3259

Revision 3247, 14.3 KB checked in by jukka, 7 years ago (diff)

Small fixes to unicode and parsing errors.

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
19from AccessControl import ClassSecurityInfo, getSecurityManager
20from Products.Archetypes.public import *
21from Products.CMFCore.utils import getToolByName
22from config import PROJECTNAME, MODIFY_CONTENT, VIEW, to_unicode
23from FieldsWidgets import ChapterField, ExerciseWidget
24from Schemata import no_description, material_schema, community_editing_schema, draft_schema
25from Material import Material
26from permissions import MODIFY_CONTENT
27from random import shuffle
28from Products.LeMill import LeMillMessageFactory as _
29from Products.MailHost.MailHost import MailHostError
30import re
31
32exercise_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
54schema = material_schema + community_editing_schema + no_description + draft_schema + exercise_schema
55schema = schema.copy()
56schema.moveField('rights', pos='bottom')
57schema.moveField('language', after='bodyText')
58schema['title'].required = True
59schema.moveField('hideDrafts', before='rights')
60
61blanks= re.compile("(\{.*?\})")
62
63class 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(to_unicode(a))
194        results.append(u'\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
330registerType(ExerciseMaterial, PROJECTNAME)
Note: See TracBrowser for help on using the repository browser.