source: trunk/ExerciseMaterial.py @ 3143

Revision 3143, 14.0 KB checked in by jukka, 9 years ago (diff)

Fixed unicode problem when sending exercises.

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')
60schema.moveField('editingMode', before='hideDrafts')
61
62blanks= re.compile("(\{.*?\})")
63
64class ExerciseMaterial(Material):
65    """Exercise page"""
66    schema = schema
67    meta_type = "ExerciseMaterial"
68    archetype_name = "ExerciseMaterial"
69    default_location = 'content/exercises'
70    security = ClassSecurityInfo()
71    security.declareObjectPublic()
72    aliases = {
73        '(Default)' : 'fullscreen_view',
74        'view'      : 'base_view',
75        'edit'      : 'base_edit',
76        'feedback'  : 'feedback_view',
77    }
78
79    def separateBlanksFromText(self, text):
80        pieces=[]
81        answers=[]
82        previous_was_blank=False
83        for piece in re.split(blanks, text):
84            if piece.startswith('{'):
85                answer=to_unicode(piece.strip('{} '))
86                if previous_was_blank:
87                    answers[-1].append(answer)
88                else:
89                    answers.append([answer])
90            else:
91                pieces.append(to_unicode(piece))
92        return pieces, answers
93           
94    def replaceBlanksWithInputTag(self, text, index, readonly=False, give_answers=False):
95        pieces, answers=self.separateBlanksFromText(text)
96        inputs=[]
97        results=[]
98        if readonly:
99            readonly=' readonly="1"'
100        else:
101            readonly=''
102        for i, answer in enumerate(answers):
103            if give_answers:
104                answer=''.join('{%s}' % a for a in answer)
105            else:
106                answer=""
107            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)
108            inputs.append(a)
109        for piece in pieces:
110            results.append(piece)
111            if inputs:
112                results.append(inputs.pop(0))
113        if inputs: # leftovers
114            results+=inputs
115        return u''.join(results)
116
117    def checkFillInTheBlankAnswersFromForm(self, form, text, index):
118        """ Checks fill-in-the-blanks exercise from web form,
119            returns completed exercise text and lists of correct and wrong answers """
120        pieces, answers=self.separateBlanksFromText(text)
121        corrects=wrongs=0
122        my_answers=[]
123        for i, choices in enumerate(answers):
124            a=to_unicode(form.get('exercise_%s_answer_%s' % (index, i), ''))
125            if a in choices:
126                corrects+=1
127                my_answers.append((a, '% '))
128            else:
129                wrongs+=1
130                my_answers.append((a, '* '))
131        results=[]
132        for piece in pieces:
133            results.append(piece)
134            if answers and my_answers:
135                ans, mark=my_answers.pop(0)               
136                results.append('/'.join(answers.pop(0)))
137                results.append('[%s%s]' % (mark, ans))
138        results.append('\n')
139        return u''.join(results), corrects, wrongs
140
141    def checkMultipleChoiceFromForm(self, form, question, choices, index):
142        """ Checks form for multiple choice test, returns exercise with correct answers marked and number of right and wrong answers """
143        corrects=wrongs=0
144        results=[question,'\n\n']
145        # Check if this is a choice or multiple choice form
146        cor=0
147        multiple=False
148        for q,c in choices:
149            if c:
150                cor+=1
151        if cor!=1:
152            multiple=True
153       
154        if multiple:
155            for i, choice in enumerate(choices):
156                a=form.get('exercise_%s_checkbox_%s' % (index, i), '')
157                if a:
158                    results.append('[x]')
159                    if not choice[1]:
160                        results.append(' <- [ ]\t')
161                        wrongs+=1
162                    else:
163                        results.append('\t\t')
164                        corrects+=1
165                else:
166                    results.append('[ ]')
167                    if choice[1]:
168                        results.append(' <- [x]\t')
169                        wrongs+=1
170                    else:
171                        results.append('\t\t')
172                        corrects+=1               
173                results.append(to_unicode(choice[0]))
174                results.append('\n')
175        else:
176            a=int(form.get('exercise_%s_radio' % index, '-1'))         
177            for i, choice in enumerate(choices):
178                if a==i:
179                    results.append('[x]')
180                    if choice[1]:
181                        corrects=1
182                    else:
183                        wrongs=1                       
184                    results.append('\t\t')
185                else:
186                    results.append('[ ]')
187                    if choice[1]:
188                        results.append(' <- [x]\t')
189                        wrongs=1
190                    else:
191                        results.append('\t\t')
192                results.append(to_unicode(choice[0]))
193                results.append('\n')
194        results.append('\n')
195        return u''.join(results), corrects, wrongs               
196
197       
198    def checkOpenEndedFromForm(self, form, question, index):
199        """ Reports back the given answer """
200        results=[question,'\n\n']
201        a=form.get('exercise_%s_open_ended' % index, '')
202        results.append(a)
203        results.append('\n\n')
204        return u''.join(results)                           
205
206    def checkExercise(self):
207        """ Builds a text dump about evaluating the exercise, used for web feedback and email """
208        lt=getToolByName(self, 'lemill_tool')
209        form=self.REQUEST.form
210        corrects=0
211        wrongs=0
212        results=[to_unicode(self.Title()),'\n','-'*len(self.Title()),'\n\n']
213        task=0
214        for index, chapter in enumerate(self.getBodyText()):
215            text=lt.stripHTML(to_unicode(chapter['text']))
216            cs=ws=0
217            s=''
218            if chapter['type']=='fill_in_the_blanks':
219                s, cs, ws=self.checkFillInTheBlankAnswersFromForm(form, text,index)
220            elif chapter['type']=='open_ended':
221                s=self.checkOpenEndedFromForm(form, text, index)
222            elif chapter['type']=='multiple_choices':
223                s, cs, ws=self.checkMultipleChoiceFromForm(form, text, chapter['choices'], index)
224            if s:
225                task+=1
226                corrects+=cs
227                wrongs+=ws
228                results.append('%s. ' % task)
229                results.append(s)
230        if corrects or wrongs:
231            results.append('-----------------------------------------\n')
232            results.append('                %s / %s   = %2d%%\n' % (corrects, corrects+wrongs, ((corrects*1.0)/(corrects+wrongs))*100))
233        return u''.join(results)
234
235
236    def sendAnswers(self, REQUEST):
237        """ Send e-mail to a teacher """
238        msg=self._sendAnswers(REQUEST)
239        lt=getToolByName(self, 'lemill_tool')
240        lt.addPortalMessage(msg[0])
241        return self.feedback_view()
242
243    def _sendAnswers(self, REQUEST):
244        ### Note for future unicode problems: self.translate returns unicode objects,
245        # but forms return utf-8 encoded strings. joining them together or embedding them with %s is harmful.
246        # when building longer strings, they should be manipulated as unicode (convert utf-8 strings with to_unicode()), but when
247        # sending email, the text should be utf-8 encoded str.   
248        mhost=self.MailHost
249        body=[]
250        students_name = to_unicode(REQUEST.get('your_name',''))
251        #students_email = to_unicode(REQUEST.get('students_email',''))
252        students_email= REQUEST.get('students_email','')
253        teachers_email = REQUEST.get('teachers_email','')
254        captcha_c=REQUEST.get('recaptcha_challenge_field','')
255        captcha_r=REQUEST.get('recaptcha_response_field','')
256        captcha=self.lemill_tool.validateCaptcha(captcha_c, captcha_r)
257        d={'name':students_name,
258            'exercise_title':to_unicode(self.Title()),
259            'exercise_url':to_unicode(self.absolute_url())}     
260        if not students_email or (teachers_email and not students_name):
261            msg = self.translate("text_mail_feedback_no_information", "You have not provided enough information to send an e-mail.",domain='lemill')
262            return (msg, 'warn')
263        if not captcha:
264            msg = self.translate("Invalid answer for humanity test.",domain='lemill')
265            return (msg, 'warn')
266        # Build message body
267        if teachers_email:
268            body.append(self.translate(u"""Hello,
269%(name)s has completed the LeMill exercise '%(exercise_title)s' at %(exercise_url)s.
270""",domain='lemill') % d)
271        else:
272            body.append(self.translate(u"""Hello %(name)s,
273You have completed the LeMill exercise '%(exercise_title)s' at %(exercise_url)s.
274""",domain='lemill') % d)
275        body.append(self.translate(u"text_mail_feedback_message_body_explanation", u"If you are surprised by getting this e-mail, you can simply ignore and delete it as someone may have accidentally inserted your address as a recipient.",domain='lemill'))
276        body.append('\n\n')
277        body.append(self.checkExercise())
278        body.append('\n\n')
279        body.append(self.translate(u"text_mail_feedback_message_body_regards", "Best regards, LeMill", domain='lemill'))                     
280        body=u''.join(body)
281        # Other email fields
282        if students_name:
283            msubject=self.translate(u"LeMill exercise '%(exercise_title)s' by %(name)s", domain='lemill') % d
284        else:
285            msubject=self.translate(u"LeMill exercise '%(exercise_title)s' feedback", domain='lemill') % d
286        if teachers_email:
287            mto=teachers_email
288            mcc=students_email
289        else:
290            mto=students_email
291            mcc=''
292        # Send
293        try:
294            body=body.encode('utf-8')
295            mhost.secureSend(body, mto, "LeMill <no-reply@lemill.net>", subject=msubject, mcc=mcc, charset='utf-8')
296            msg = self.translate("text_mail_feedback_message_sent", "The e-mail has been sent.",domain='lemill')
297            return (msg, 'mess')
298        except (MailHostError, AttributeError):
299            msg = self.translate("The e-mail could not be sent.",domain='lemill')
300            return (msg, 'warn')
301
302    def prepareForPDF(self):
303        """ Replaces form fields with dotted lines """       
304        text=Material.prepareForPDF(self)
305        # Getting rid of submit for exercises
306        submit_pattern = re.compile('<fieldset class="visualNoPrint">.*?</fieldset>', re.MULTILINE | re.DOTALL | re.IGNORECASE)
307        text = re.sub(submit_pattern, '', text)
308        # Replace text inputs with some dots
309        input_text_pattern = re.compile('<input type="text" value="" name="exercise_.*?/>',re.DOTALL | re.IGNORECASE | re.MULTILINE)
310        text = re.sub(input_text_pattern, '.......... ', text)
311        # Replace textarea with some lines of dots
312        textarea_pattern = re.compile('<textarea.*?</textarea>', re.DOTALL | re.IGNORECASE | re.MULTILINE)
313        text = re.sub(textarea_pattern, '<br />'.join(('.'*80, '.'*80)), text)
314        return text
315
316    def fixBrokenChoice(self):
317        """ If choice -type is mapped wrong, fix it """
318        old_values=self.getRawBodyText()
319        new_values=[]
320        fixed=False
321        for vald in old_values:
322            if vald['type']=='choice' and isinstance(vald['text'], list):
323                 # [text, [correct_answers], [wrong_answers]]
324                 broken_text=vald['text']
325                 vald['text']=broken_text[0]
326                 choices=[(broken_text[1][0], 1)]
327                 for item in broken_text[2]:
328                    choices.append((item, 0))
329                 vald['choices']=choices
330                 vald['type']='multiple_choices'
331                 fixed=True
332            new_values.append(vald)
333        if fixed:
334            self.setBodyText(new_values)
335
336
337registerType(ExerciseMaterial, PROJECTNAME)
Note: See TracBrowser for help on using the repository browser.