source: trunk/ExerciseMaterial.py @ 2421

Revision 2421, 16.9 KB checked in by anonymous, 11 years ago (diff)

References #1758 not ready yet, but getting closer, also forgot to add new classes to css

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
22
23from config import PROJECTNAME, MODIFY_CONTENT, VIEW, to_unicode
24from FieldsWidgets import ChapterField, ChapterWidget, StringWidget
25from SharedMetadata import *
26from Material import Material
27from permissions import MODIFY_CONTENT
28from random import shuffle
29from Products.LeMill import LeMillMessageFactory as _
30
31import re
32
33schema = BaseSchema + Schema((
34   ChapterField('bodyText',
35        accessor="getBodyText",
36        edit_accessor = 'getRawBodyText',
37        mutator = "setBodyText",
38        index='ZCTextIndex:schema',
39        index_method = 'getOnlyText',
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=[('','guidelines'),],
47        widget=ExerciseWidget(label = "Body text",
48            label_msgid = "label_bodytext",
49            i18n_domain = "lemill",
50            ),
51    ),
52))
53
54schema = schema + tags + language_schema + group_sharing + subject_area_schema + target_group_schema + license_schema + coverImage + lemill_metadata_mods + no_description + author_schema + deletionReason + version_schema + translation_schema + latest_edit_schema + score + state
55
56schema = schema.copy()
57schema.moveField('rights', pos='bottom')
58schema.moveField('language', after='bodyText')
59schema['title'].required = True
60
61fill_in_the_blanks=re.compile(r"""(?P<filler>({.*?})+)""",  re.IGNORECASE)
62
63class ExerciseMaterial(Material):
64    """Exercise page"""
65
66    schema = schema
67
68    meta_type = "ExerciseMaterial"
69    archetype_name = "ExerciseMaterial"
70
71    security = ClassSecurityInfo()
72    security.declareObjectPublic()
73
74    security.declarePrivate('manage_afterAdd')
75    def manage_afterAdd(self, item, container):
76        Material.manage_afterAdd(self, item, container)
77
78    def getOnlyText(self):
79        field=self.getField('bodyText')
80        values = field.get(self)
81        dump= '\n'.join([x[0] for x in values if x[1] not in ['media_piece', 'multiple_choices']])
82        return dump
83
84    def getOnlyRawText(self):
85        field=self.getField('bodyText')
86        values = field.getRaw(self)
87        dump= '\n'.join([x[0] for x in values if x[1] not in ['media_piece', 'multiple_choices']])
88        return dump
89
90
91    def isCorrectAnswer(self, chapter, answer_index):
92        """ given a chapter having a multiple choice question, is this answer in correct answers? """
93        return answer_index < len(chapter[1])
94       
95
96    security.declareProtected(MODIFY_CONTENT,'delChapter')
97    def delChapter(self, REQUEST):
98        """ delete chapter """
99        field = self.getField('bodyText')
100        field.delChapter(self, int(REQUEST.get('delete')))
101        return REQUEST.RESPONSE.redirect(self.absolute_url()+'/edit')
102
103    def getAllAnswers(self, chapter):
104        """ Will return the list of the shuffled combined correct and incorrect answers """
105        # Chapter should have the structure: [question,correct_list,incorrect_list]
106        all_answers = chapter[1] + chapter[2]
107        extended_answers = []
108        for numerated_answer in enumerate(all_answers):
109            extended_answers.append(numerated_answer)
110        shuffle(extended_answers)
111        return extended_answers
112
113    def get_values_from_fitbs(self, chapter_index):
114        """ find words in {}:s and return them as dict """
115       
116        text=self.getBodyText()
117        text=text[chapter_index][0]
118        matches=fill_in_the_blanks.findall(text)
119        results={}
120        index=0
121        for m in matches:
122            answer=m[0]
123            answer=answer.strip(' {}')
124            answer=answer.split('}{')
125            results['exercise_%s_answer_%s' % (chapter_index, index)]=answer
126            index=index+1
127        return results
128           
129    def replace_blanks_with_input_tag(self, chapter_index, answers=True, readonly=True):
130        """ find words in { } and replace them with input boxes """
131        replacement="""<input type="text" value="%s"%s name="exercise_%s_answer_%s" id="exercise_%s_answer_%s" />"""
132        self.iterator=0
133       
134        def rep(match):
135            answer_index=self.iterator
136            if answers:
137                value=match.group('filler').strip(' {}')
138                value=value.replace('}{','/')
139            else:
140                value=""
141            self.iterator+=1
142            return replacement % (value, readonly, chapter_index, answer_index, chapter_index, answer_index)
143
144        if readonly:
145            readonly=' readonly="1"'
146        else:
147            readonly=''           
148        text=self.getBodyText()
149        text=text[chapter_index][0]
150        text=fill_in_the_blanks.sub(rep, text)
151        del self.iterator
152        return text
153
154    def replace_blanks_with_your_answers(self, text, your_answerdict, i):
155        """ find braces {text} and add own answers before them in brackets [mytext]{text} """
156        self.iterator=0
157       
158        def rep(match):
159            value=match.group('filler')           
160            your_answer=your_answerdict.get('exercise_%s_answer_%s' % (i, self.iterator), '')
161            self.iterator+=1
162            return '[%s]%s' % (your_answer, value)
163        text=fill_in_the_blanks.sub(rep, text)
164        del self.iterator
165        return text
166
167    def replace_blanks_with_feedback(self, text, your_answerdict, i):
168        """ prepare the exercise for web-based feedback, make a html version out of it """
169        fitb_correct_repl = u"""<span class="correct_fitb">%s</span>"""
170        fitb_incorrect_repl = u"""<span class="incorrect_fitb">%s</span> <span class="corrected_fitb">%s</span>"""
171        self.iterator = 0
172
173        def rep(match):
174            value = match.group('filler')
175            your_answer_all = your_answerdict.get('exercise_%s_answer_%s' % (i, self.iterator), '')
176            your_answer = your_answer_all[0]
177            self.iterator+=1
178            if your_answer_all[1] == 'correct':
179                return fitb_correct_repl % (to_unicode(your_answer.strip()))
180            elif your_answer_all[1] == 'incorrect':
181                return fitb_incorrect_repl % (to_unicode(your_answer.strip()), value)
182        text = fill_in_the_blanks.sub(rep, text)
183        del self.iterator
184        return text
185
186
187    def showWebFeedback(self, REQUEST):
188        """ Deals with the web-based feedback """
189        message = (None, 'warn')
190        feedbody = u''
191        multiple_choices_repl = u"""<span class="%s"><input type="checkbox" value="%s" name="exercise_%s_answer_%s" id="exercise_%s_answer_%s" %s />%s</span><br />"""
192
193        exercise_body = self.getBodyText()
194
195        # If the form has not ben submited -> only this mesage will be shown
196        if not REQUEST.get('submit_web_feedback',''):
197            msg = _(u"Unable to generate the feedback. You have not submited the form.")
198            return (feedbody, (msg, 'warn'))
199
200        # Go through the exercise body and generate html for multiple_choices and fill_in_the_blanks types
201        i = 0
202        for (chapter_text, chapter_type) in exercise_body:
203            if chapter_type == 'multiple_choices':
204                # Use mchoice_body as a holder to construct the htm for the question -> then append it to the general holder
205                mchoice_body = u''
206                correct_answers = chapter_text[1]
207                answers_total = correct_answers + chapter_text[2]
208                for a in range(len(correct_answers)):
209                    answer_value = REQUEST.get('exercise_%s_checkbox_%s' % (i,a),'')
210                    if answer_value:
211                        mchoice_body += multiple_choices_repl % ('correct_mc', '0', i, a, i, a, 'checked="checked"', correct_answers[a])
212                    else:
213                        mchoice_body += multiple_choices_repl % ('incorrect_mc', '0', i, a, i, a, '', correct_answers[a])
214                for a in range(len(correct_answers),len(answers_total)):
215                    answer_value = REQUEST.get('exercise_%s_checkbox_%s' % (i,a),'')
216                    if answer_value:
217                        mchoice_body += multiple_choices_repl % ('incorrect_mc', '0', i, a, i, a, 'checked="checked"', answers_total[a])
218                    else:
219                        mchoice_body += multiple_choices_repl % ('correct_mc', '0', i, a, i, a, '', answers_total[a])
220                # Add the html result to the general holder
221                feedbody += u"""<p>%s</p> <p>%s</p>""" % (chapter_text[0], mchoice_body)
222            elif chapter_type == 'fill_in_the_blanks':
223                correct_answer_dict = self.get_values_from_fitbs(i)
224                my_answer_dict = {}
225                all_answers_n = len(correct_answer_dict.keys())
226                for it in range(all_answers_n):
227                    my_answer = REQUEST.get('exercise_%s_answer_%s' % (i,it),'')
228                    correct_answer = [to_unicode(c.strip()) for c in correct_answer_dict['exercise_%s_answer_%s' % (i,it)]]
229                    if to_unicode(my_answer.strip()) in correct_answer:
230                        my_answer_dict['exercise_%s_answer_%s' % (i,it)]=(my_answer, 'correct')
231                    else:
232                        my_answer_dict['exercise_%s_answer_%s' % (i,it)]=(my_answer, 'incorrect')
233                # Using external helper method to replace the blanks and construct the html
234                exercise_text = self.replace_blanks_with_feedback(chapter_text, my_answer_dict, i)
235                # Add the html result to the general holder
236                feedbody += u"""%s""" % (exercise_text)
237
238            i += 1 # increase the thing
239        # If there is a user e-mail -> we try to get the e-mail sending functionality going
240        # All the other checks will be defined there, as it was before this web-based feedback
241        if REQUEST.get('students_email', ''):
242            message = self.sendAnswers(REQUEST)
243        # XXX Create an alias name "feedback" for "feedback_view"
244        return (feedbody, message)
245       
246    def sendAnswers(self, REQUEST):
247        """ Send e-mail to a teacher """
248        putils = getToolByName(self,'plone_utils')
249        letool = getToolByName(self,'lemill_tool')
250        mhost = putils.getMailHost()
251        mbody = u''
252        students_name = REQUEST.get('your_name','')
253        students_email = REQUEST.get('students_email','')
254        teachers_email = REQUEST.get('teachers_email','')
255
256        if not students_email or (teachers_email and not students_name):
257            msg = _(u"You have not provided enough information to send an e-mail.")
258            # Messages are translated here, every message should also have a type along with it
259            return (msg, 'warn')
260
261        if not self.lemill_tool.validateCaptcha(REQUEST.get('captcha')):
262            msg = _(u"Invalid answer for humanity test.")
263            return (msg, 'warn')
264       
265        exercise_body = self.getBodyText()
266
267
268        # Now we should compose the message body       
269        if teachers_email:
270            mbody += _(u"""
271Hello,
272
273%(name)s has completed the LeMill exercise '%(exercise_title)s' at %(exercise_url)s.
274""") % {'name':to_unicode(students_name), 'exercise_title':to_unicode(self.Title()), 'exercise_url':to_unicode(self.absolute_url())}
275        else:
276            mbody += _(u"""
277Hello %(name)s,
278
279You have completed the LeMill exercise '%(exercise_title)s' at %(exercise_url)s.
280""") % {'name':to_unicode(students_name), 'exercise_title':to_unicode(self.Title()), 'exercise_url':to_unicode(self.absolute_url())}
281
282        mbody += _(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.")
283        # Message body is composed chapter by chapter
284        i=0
285        for (chapter_text, chapter_type) in exercise_body:
286            if chapter_type=='multiple_choices':
287                answered_right = 0
288                answered_wrong = 0
289                question = chapter_text[0]
290                correct_answers = chapter_text[1]
291                answers_total = correct_answers+chapter_text[2]
292                my_answers=[]
293                # Correct answers have small index number
294                for a in range(len(correct_answers)):
295                    answer_value = REQUEST.get('exercise_%s_checkbox_%s' % (i,a),'')
296                    if answer_value:
297                        my_answers.append(answers_total[a])
298                        answered_right+=1
299                    else:
300                        answered_wrong+=1
301                # These are all wrong answers
302                for a in range(len(correct_answers),len(answers_total)):
303                    answer_value = REQUEST.get('exercise_%s_checkbox_%s' % (i,a),'')
304                    if answer_value:
305                        my_answers.append(answers_total[a])
306                        answered_wrong+=1
307                    else:
308                        answered_right+=1
309                percentage=int((float(answered_right)/(answered_wrong+answered_right))*100)
310                mbody += _(u"""
311
312Your answer to question '%(question)s' was:
313%(my_answers)s
314The correct answer is:
315%(correct_answers)s
316You got %(percentage)s%% correct!
317""") % {'question':to_unicode(question), 'my_answers':to_unicode(', '.join(my_answers)), 'correct_answers':to_unicode(', '.join(correct_answers)), 'percentage':percentage}
318
319            elif chapter_type == 'fill_in_the_blanks':
320                correct_answer_dict=self.get_values_from_fitbs(i)
321                correct_answers_n=0
322                my_answer_dict={}
323                all_answers_n=len(correct_answer_dict.keys())
324                for it in range(all_answers_n):
325                    my_answer=REQUEST.get('exercise_%s_answer_%s' % (i,it),'')
326                    correct_answer=[to_unicode(c.strip()) for c in correct_answer_dict['exercise_%s_answer_%s' % (i,it)]]
327                    if to_unicode(my_answer.strip()) in correct_answer:
328                        correct_answers_n+=1
329                    my_answer_dict['exercise_%s_answer_%s' % (i,it)]=my_answer
330                exercise_text=self.replace_blanks_with_your_answers(chapter_text, my_answer_dict, i)
331                exercise_text=letool.html_to_text(exercise_text)
332                mbody += _(u"""
333Answer entered by student is in the square brackets. The correct answer is in the logical brackets.
334In the following fill-in-the-blanks exercise your answers and correct answers were:
335%(exercise_text)s
336
337You got %(correct_answers_n)s/%(all_answers_n)s correct!
338""") % {'exercise_text':to_unicode(exercise_text), 'correct_answers_n':correct_answers_n, 'all_answers_n':all_answers_n}           
339
340            elif chapter_type == 'open_ended':
341                students_answer = REQUEST.get('exercise_%s_answer' % i, '')
342                mbody += _(u"""
343Your answer to question '%(question)s' was:
344%(answer)s
345
346Your teacher will give you feedback on your answer.
347""") % {'question':to_unicode(chapter_text), 'answer':to_unicode(students_answer)}
348            i+=1 # <--- remember to increase index
349           
350        mbody += _(u"\n\nBest regards, LeMill")
351        mbody = mbody.encode('utf-8')
352
353        if teachers_email:
354            message_from = "%s <%s>" % (students_name, students_email)
355            message_subject = "LeMill exercise '%s' by %s" % (self.Title(),students_name)
356            try:
357                mhost.secureSend(mbody, mto=teachers_email, mfrom=message_from, subject=message_subject, mcc=students_email, charset='utf-8')
358                msg = _(u"The e-mail has been sent to you and a teacher.")
359                return (msg, 'mess')
360            except:
361                msg = _(u"The e-mail could not be sent.")
362                return (msg, 'warn')
363        else:
364            message_from = "%s <%s>" % ('LeMill', students_email)
365            message_subject = "LeMill exercise '%s' feedback" % (self.Title())
366            try:
367                mhost.secureSend(mbody, mto=students_email, mfrom=message_from, subject=message_subject, charset='utf-8')
368                msg = _(u"The e-mail has been sent to you.")
369                return (msg, 'mess')
370            except:
371                msg = _(u"The e-mail could not be sent.")
372                return (msg, 'warn')
373
374
375registerType(ExerciseMaterial, PROJECTNAME)
Note: See TracBrowser for help on using the repository browser.