Merge lp:~ellimistd/schooltool.quiz/SchoolToolQuizDev into lp:schooltool.quiz

Proposed by David Reich
Status: Needs review
Proposed branch: lp:~ellimistd/schooltool.quiz/SchoolToolQuizDev
Merge into: lp:schooltool.quiz
Diff against target: 2678 lines (+1242/-563) (has conflicts)
17 files modified
src/schooltool/quiz/browser/deployedquiz.py (+29/-2)
src/schooltool/quiz/browser/quiz.py (+845/-475)
src/schooltool/quiz/browser/quizitem.py (+26/-3)
src/schooltool/quiz/browser/stests/quiz_deployment.txt (+26/-7)
src/schooltool/quiz/browser/stests/quiz_duplicate.txt (+15/-4)
src/schooltool/quiz/browser/stests/quiz_export.txt (+5/-0)
src/schooltool/quiz/browser/stests/quiz_import.txt (+7/-1)
src/schooltool/quiz/browser/stests/quiz_management.txt (+114/-3)
src/schooltool/quiz/browser/stests/student_answers.txt (+29/-0)
src/schooltool/quiz/browser/stests/test_selenium.py (+1/-1)
src/schooltool/quiz/browser/templates/answers.pt (+4/-4)
src/schooltool/quiz/browser/templates/quiz_item_body.pt (+5/-3)
src/schooltool/quiz/browser/templates/quiz_item_form_script.pt (+5/-1)
src/schooltool/quiz/quiz.py (+5/-0)
src/schooltool/quiz/quizitem.py (+1/-0)
src/schooltool/quiz/rational.py (+125/-0)
src/schooltool/quiz/utils.py (+0/-59)
Text conflict in src/schooltool/quiz/browser/quiz.py
Text conflict in src/schooltool/quiz/quiz.py
To merge this branch: bzr merge lp:~ellimistd/schooltool.quiz/SchoolToolQuizDev
Reviewer Review Type Date Requested Status
Douglas Cerna Pending
Review via email: mp+149160@code.launchpad.net

Description of the change

Changed tests (quiz_duplicate, quiz_management, quiz_deployment, quiz_import, quiz_export, and student_answers) to account for rational tests. I also fixed bugs that had been indicated by failing tests in quiz_import and quiz_duplicate, but quiz_export is still not functional.

To post a comment you must log in.
39. By David Reich <email address hidden>

changed tests to account for rational questions

Unmerged revisions

39. By David Reich <email address hidden>

changed tests to account for rational questions

38. By David Reich <email address hidden>

Added rational questions, with automatic validation (No tests yet)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/schooltool/quiz/browser/deployedquiz.py'
2--- src/schooltool/quiz/browser/deployedquiz.py 2013-01-23 23:42:02 +0000
3+++ src/schooltool/quiz/browser/deployedquiz.py 2013-03-04 22:05:25 +0000
4@@ -78,6 +78,7 @@
5 from schooltool.quiz.utils import render_rst, render_pre
6 from schooltool.quiz.browser.quiz import SolutionDoneLinkViewlet
7 from schooltool.quiz.utils import render_date
8+from schooltool.quiz.utils import RationalValidator
9
10
11 def getStartTime(adapter):
12@@ -941,8 +942,10 @@
13 return self.createOpenQuestionField(item)
14 elif term.token == 'multiple':
15 return self.createMultipleSelectionField(item)
16- else:
17+ elif term.token == 'selection':
18 return self.createSelectionField(item)
19+ else:
20+ return self.createRationalQuestionField(item)
21
22 def createOpenQuestionField(self, item):
23 name = item.__name__.encode('utf-8')
24@@ -953,6 +956,16 @@
25 datamanager.DictionaryField(self.values, schema_field)
26 result = field.Fields(schema_field)
27 return result
28+
29+ def createRationalQuestionField(self, item):
30+ name = item.__name__.encode('utf-8')
31+ schema_field = interfaces.ReStructuredText(
32+ __name__=name,
33+ description=item.body)
34+ self.values[name] = None
35+ datamanager.DictionaryField(self.values, schema_field)
36+ result = field.Fields(schema_field)
37+ return result
38
39 def choicesVocabulary(self, item, shuffle=False):
40 terms = []
41@@ -1013,8 +1026,10 @@
42 grade = UNSCORED
43 elif term.token == 'multiple':
44 grade = self.calculateMultipleSelectionGrade(item, answer)
45- else:
46+ elif term.token == "selection":
47 grade = self.calculateSelectionGrade(item, answer)
48+ else:
49+ grade = self.calculateRationalGrade(item, answer)
50 evaluation = AnsweredEvaluation(item, answer, grade)
51 evaluations.addEvaluation(evaluation)
52 self.request.response.redirect(self.nextURL())
53@@ -1041,6 +1056,11 @@
54 if choice.correct and choice in given_choices:
55 return 100
56 return 0
57+
58+ def calculateRationalGrade(self, item, answer):
59+ if RationalValidator(item.solution, answer):
60+ return 100
61+ return 0
62
63 @button.buttonAndHandler(_('Cancel'), name='cancel')
64 def handle_cancel(self, action):
65@@ -1132,6 +1152,13 @@
66 term = self.types_vocabulary.getTerm(item.type_)
67 return term.token == 'open'
68
69+ def is_rational(self, item):
70+ term = self.types_vocabulary.getTerm(item.type_)
71+ return term.token == 'rational'
72+
73+ def has_open_view(self, item):
74+ return self.is_rational(item) or self.is_open(item)
75+
76 def is_graded(self, evaluation):
77 return evaluation and evaluation.value not in (UNSCORED, None)
78
79
80=== modified file 'src/schooltool/quiz/browser/quiz.py'
81--- src/schooltool/quiz/browser/quiz.py 2013-01-24 10:01:26 +0000
82+++ src/schooltool/quiz/browser/quiz.py 2013-03-04 22:05:25 +0000
83@@ -73,56 +73,72 @@
84 choices_column_separator = '**|**'
85
86
87+<<<<<<< TREE
88+=======
89+class PersonSections(object):
90+
91+ person = None
92+
93+ @Lazy
94+ def learner_sections(self):
95+ return list(ILearner(self.person).sections())
96+
97+ @Lazy
98+ def instructor_sections(self):
99+ return list(IInstructor(self.person).sections())
100+
101+
102+>>>>>>> MERGE-SOURCE
103 class QuizContainerAbsoluteURLAdapter(BrowserView):
104-
105- adapts(interfaces.IQuizContainer, IBrowserRequest)
106- implements(IAbsoluteURL)
107-
108- def __str__(self):
109- app = ISchoolToolApplication(None)
110- return '%s/quizzes' % absoluteURL(app, self.request)
111-
112- __call__ = __str__
113+ adapts(interfaces.IQuizContainer, IBrowserRequest)
114+ implements(IAbsoluteURL)
115+
116+ def __str__(self):
117+ app = ISchoolToolApplication(None)
118+ return '%s/quizzes' % absoluteURL(app, self.request)
119+
120+ __call__ = __str__
121
122
123 class QuizzesLink(flourish.page.LinkViewlet, PersonSections):
124
125- startup_view_name = 'quizzes.html'
126-
127- @Lazy
128- def person(self):
129- return IPerson(self.request.principal, None)
130-
131- @property
132- def enabled(self):
133- if self.person is None:
134- return False
135- editable_quizzes = getRelatedObjects(
136- self.person, relationships.URIQuiz, relationships.URIQuizEditors)
137- return self.instructor_sections or \
138- self.learner_sections or \
139- editable_quizzes
140-
141- @property
142- def url(self):
143- if self.person is None:
144- return ''
145- return '%s/%s' % (absoluteURL(self.person, self.request),
146- self.startup_view_name)
147+ startup_view_name = 'quizzes.html'
148+
149+ @Lazy
150+ def person(self):
151+ return IPerson(self.request.principal, None)
152+
153+ @property
154+ def enabled(self):
155+ if self.person is None:
156+ return False
157+ editable_quizzes = getRelatedObjects(
158+ self.person, relationships.URIQuiz, relationships.URIQuizEditors)
159+ return self.instructor_sections or \
160+ self.learner_sections or \
161+ editable_quizzes
162+
163+ @property
164+ def url(self):
165+ if self.person is None:
166+ return ''
167+ return '%s/%s' % (absoluteURL(self.person, self.request),
168+ self.startup_view_name)
169
170
171 def taken(person, deployed):
172- result = False
173- evaluations = IEvaluations(removeSecurityProxy(person))
174- for item in deployed:
175- evaluation = evaluations.get(item, None)
176- if evaluation is not None:
177- return True
178- return result
179+ result = False
180+ evaluations = IEvaluations(removeSecurityProxy(person))
181+ for item in deployed:
182+ evaluation = evaluations.get(item, None)
183+ if evaluation is not None:
184+ return True
185+ return result
186
187
188 class PersonQuizzesView(flourish.page.Page, PersonSections):
189
190+<<<<<<< TREE
191 implements(interfaces.ISchoolToolQuizView,
192 interfaces.IPersonQuizView)
193
194@@ -176,163 +192,228 @@
195
196 def taken(self, deployed):
197 return taken(self.context, deployed)
198+=======
199+ implements(interfaces.ISchoolToolQuizView)
200+
201+ container_class = 'container widecontainer'
202+
203+ @Lazy
204+ def person(self):
205+ return self.context
206+
207+ @property
208+ def quizContainer(self):
209+ app = ISchoolToolApplication(None)
210+ return interfaces.IQuizContainer(app)
211+
212+ @Lazy
213+ def editable_quizzes(self):
214+ result = []
215+ for quiz in self.quizContainer.values():
216+ if self.person in quiz.editors:
217+ result.append(quiz)
218+ return result
219+
220+ @Lazy
221+ def deployed_quizzes(self):
222+ result = []
223+ for section in self.instructor_sections:
224+ container = interfaces.IDeployedQuizContainer(section)
225+ for deployed in container.values():
226+ result.append(deployed)
227+ return result
228+
229+ @Lazy
230+ def takeable_quizzes(self):
231+ result = []
232+ for section in self.learner_sections:
233+ container = interfaces.IDeployedQuizContainer(section)
234+ for deployed in container.values():
235+ if not self.taken(deployed):
236+ result.append(deployed)
237+ return result
238+
239+ @Lazy
240+ def taken_quizzes(self):
241+ result = []
242+ for section in self.learner_sections:
243+ container = interfaces.IDeployedQuizContainer(section)
244+ for deployed in container.values():
245+ if self.taken(deployed):
246+ result.append(deployed)
247+ return result
248+
249+ def taken(self, deployed):
250+ return taken(self.context, deployed)
251+>>>>>>> MERGE-SOURCE
252
253
254 class QuizContainerView(flourish.page.Page):
255
256- def update(self):
257- print list(self.context.keys())
258+ def update(self):
259+ print list(self.context.keys())
260
261
262 class QuizContainerAddLinks(flourish.page.RefineLinksViewlet):
263
264- pass
265+ pass
266
267
268 class QuizContainerActionsLinks(flourish.page.RefineLinksViewlet):
269
270- pass
271+ pass
272
273
274 class QuizAddLinks(flourish.page.RefineLinksViewlet):
275
276- pass
277+ pass
278
279
280 class QuizViewLinks(flourish.page.RefineLinksViewlet):
281
282- pass
283+ pass
284
285
286 class QuizActionLinks(flourish.page.RefineLinksViewlet):
287
288- pass
289+ pass
290
291
292 class QuizAddLink(flourish.page.LinkViewlet):
293
294- @property
295- def url(self):
296- app = ISchoolToolApplication(None)
297- container = interfaces.IQuizContainer(app)
298- return '%s/add.html' % absoluteURL(container, self.request)
299+ @property
300+ def url(self):
301+ app = ISchoolToolApplication(None)
302+ container = interfaces.IQuizContainer(app)
303+ return '%s/add.html' % absoluteURL(container, self.request)
304
305
306 class QuizDeployLinkViewlet(flourish.page.LinkViewlet):
307
308- @property
309- def enabled(self):
310- return len(list(self.context))
311+ @property
312+ def enabled(self):
313+ return len(list(self.context))
314
315
316 class PersonQuizAddLink(QuizAddLink, PersonSections):
317
318- @Lazy
319- def person(self):
320- return IPerson(self.request.principal, None)
321-
322- @property
323- def enabled(self):
324- if self.person is not None:
325- return self.instructor_sections
326- return False
327-
328- @property
329- def url(self):
330- app = ISchoolToolApplication(None)
331- container = interfaces.IQuizContainer(app)
332- camefrom = '%s/quizzes.html' % absoluteURL(self.context, self.request)
333- return '%s/add.html?camefrom=%s' % (
334- absoluteURL(container, self.request), camefrom)
335+ @Lazy
336+ def person(self):
337+ return IPerson(self.request.principal, None)
338+
339+ @property
340+ def enabled(self):
341+ if self.person is not None:
342+ return self.instructor_sections
343+ return False
344+
345+ @property
346+ def url(self):
347+ app = ISchoolToolApplication(None)
348+ container = interfaces.IQuizContainer(app)
349+ camefrom = '%s/quizzes.html' % absoluteURL(self.context, self.request)
350+ return '%s/add.html?camefrom=%s' % (
351+ absoluteURL(container, self.request), camefrom)
352
353
354 class PersonQuizImportLink(QuizAddLink, PersonSections):
355
356- @Lazy
357- def person(self):
358- return IPerson(self.request.principal, None)
359-
360- @property
361- def enabled(self):
362- if self.person is not None:
363- return self.instructor_sections
364- return False
365-
366- @property
367- def url(self):
368- app = ISchoolToolApplication(None)
369- container = interfaces.IQuizContainer(app)
370- camefrom = '%s/quizzes.html' % absoluteURL(self.context, self.request)
371- return '%s/import.html?camefrom=%s' % (
372- absoluteURL(container, self.request), camefrom)
373+ @Lazy
374+ def person(self):
375+ return IPerson(self.request.principal, None)
376+
377+ @property
378+ def enabled(self):
379+ if self.person is not None:
380+ return self.instructor_sections
381+ return False
382+
383+ @property
384+ def url(self):
385+ app = ISchoolToolApplication(None)
386+ container = interfaces.IQuizContainer(app)
387+ camefrom = '%s/quizzes.html' % absoluteURL(self.context, self.request)
388+ return '%s/import.html?camefrom=%s' % (
389+ absoluteURL(container, self.request), camefrom)
390
391
392 class QuizSolutionLinkViewlet(flourish.page.LinkViewlet):
393
394- @property
395- def enabled(self):
396- return len(list(self.context))
397+ @property
398+ def enabled(self):
399+ return len(list(self.context))
400
401
402 class QuizView(flourish.page.Page):
403
404- implements(interfaces.ISchoolToolQuizView,
405- interfaces.IMathJaxView)
406-
407- container_class = 'container widecontainer'
408-
409- @property
410- def title(self):
411- return self.context.title
412-
413- @property
414- def can_modify(self):
415- person = IPerson(self.request.principal, None)
416- return person in self.context.editors
417-
418- @Lazy
419- def quiz_items(self):
420- return list(self.context)
421-
422- @Lazy
423- def deployed_quizzes(self):
424- return list(self.context.deployed)
425+ implements(interfaces.ISchoolToolQuizView,
426+ interfaces.IMathJaxView)
427+
428+ container_class = 'container widecontainer'
429+
430+ @property
431+ def title(self):
432+ return self.context.title
433+
434+ @property
435+ def can_modify(self):
436+ person = IPerson(self.request.principal, None)
437+ return person in self.context.editors
438+
439+ @Lazy
440+ def quiz_items(self):
441+ return list(self.context)
442+
443+ @Lazy
444+ def deployed_quizzes(self):
445+ return list(self.context.deployed)
446
447
448 class EditableQuizzesViewlet(flourish.viewlet.Viewlet, PersonSections):
449
450- template = InlineViewPageTemplate('''
451- <tal:block i18n:domain="schooltool.quiz"
452- tal:define="quizzes view/view/editable_quizzes">
453- <h3 i18n:translate="">Editable Quizzes</h3>
454- <div tal:condition="quizzes"
455- tal:define="ajax nocall:view/view/providers/ajax"
456- tal:content="structure ajax/view/context/editable_quizzes_table"
457- />
458- <p i18n:translate="" tal:condition="not:quizzes">
459- There are no quizzes you can edit yet.
460- </p>
461- </tal:block>
462- ''')
463-
464- @Lazy
465- def person(self):
466- return self.context
467-
468- @property
469- def enabled(self):
470- editable_quizzes = getRelatedObjects(self.person,
471- relationships.URIQuiz,
472- relationships.URIQuizEditors)
473- return self.instructor_sections or editable_quizzes
474-
475- def render(self, *args, **kw):
476- if self.enabled:
477- return self.template(*args, **kw)
478-
479-
480+ template = InlineViewPageTemplate('''
481+ <tal:block i18n:domain="schooltool.quiz"
482+ tal:define="quizzes view/view/editable_quizzes">
483+ <h3 i18n:translate="">Editable Quizzes</h3>
484+ <div tal:condition="quizzes"
485+ tal:define="ajax nocall:view/view/providers/ajax"
486+ tal:content="structure ajax/view/context/editable_quizzes_table"
487+ />
488+ <p i18n:translate="" tal:condition="not:quizzes">
489+ There are no quizzes you can edit yet.
490+ </p>
491+ </tal:block>
492+ ''')
493+
494+ @Lazy
495+ def person(self):
496+ return self.context
497+
498+ @property
499+ def enabled(self):
500+ editable_quizzes = getRelatedObjects(self.person,
501+ relationships.URIQuiz,
502+ relationships.URIQuizEditors)
503+ return self.instructor_sections or editable_quizzes
504+
505+ def render(self, *args, **kw):
506+ if self.enabled:
507+ return self.template(*args, **kw)
508+
509+
510+<<<<<<< TREE
511+=======
512+def quiz_editors_formatter(editors, quiz, formatter):
513+ collator = ICollator(formatter.request.locale)
514+ result = sorted([person.title for person in editors],
515+ cmp=collator.cmp)
516+ return '<br />'.join(result)
517+
518+
519+>>>>>>> MERGE-SOURCE
520 class EditableQuizzesTable(table.ajax.Table):
521
522+<<<<<<< TREE
523 def items(self):
524 return self.view.editable_quizzes
525
526@@ -371,259 +452,333 @@
527 batch_size=self.batch_size,
528 prefix=self.__name__,
529 css_classes={'table': 'data editable-quizzes-table'})
530+=======
531+ table_formatter = AJAXCSSTableFormatter
532+
533+ def items(self):
534+ return self.view.editable_quizzes
535+
536+ def columns(self):
537+ default = super(EditableQuizzesTable, self).columns()
538+ questions = GetterColumn(
539+ name='questions',
540+ title=_('Questions'),
541+ getter=lambda quiz, formatter: len(list(quiz)))
542+ editors = GetterColumn(
543+ name='editors',
544+ title=_('Editors'),
545+ getter=lambda quiz, formatter: quiz.editors,
546+ cell_formatter=quiz_editors_formatter)
547+ created = GetterColumn(
548+ name='created',
549+ title=_('Created'),
550+ getter=lambda i, f: IZopeDublinCore(i).created,
551+ cell_formatter=lambda v, i, f: render_date(v),
552+ subsort=True)
553+ directlyProvides(created, ISortableColumn)
554+ duplicate = QuizActionColumn(
555+ 'duplicate',
556+ title=_('Duplicate this quiz'),
557+ action='duplicate_quiz.html',
558+ library='schooltool.quiz.flourish',
559+ image='duplicate-icon.png')
560+ return default + [questions, editors, created, duplicate]
561+
562+ def sortOn(self):
563+ return (('created', True),)
564+
565+ def updateFormatter(self):
566+ if self._table_formatter is None:
567+ self.setUp(table_formatter=self.table_formatter,
568+ batch_size=self.batch_size,
569+ prefix=self.__name__,
570+ css_classes={'table': 'data editable-quizzes-table'})
571+>>>>>>> MERGE-SOURCE
572
573
574 class QuizAddView(flourish.form.AddForm):
575
576- legend = _('Quiz Information')
577- fields = field.Fields(interfaces.IQuiz['title'])
578- content_template = ViewPageTemplateFile('templates/form.pt')
579-
580- def updateActions(self):
581- super(QuizAddView, self).updateActions()
582- self.actions['add'].addClass('button-ok')
583- self.actions['cancel'].addClass('button-cancel')
584-
585- def create(self, data):
586- quiz = Quiz()
587- form.applyChanges(self, quiz, data)
588- return quiz
589-
590- def add(self, quiz):
591- chooser = INameChooser(self.context)
592- name = chooser.chooseName('', quiz)
593- self.context[name] = quiz
594- self.setCreator(quiz)
595- self._quiz = quiz
596- return quiz
597-
598- def nextURL(self):
599- if self._finishedAdd:
600- url = absoluteURL(self._quiz, self.request)
601- else:
602- url = self.request.get('camefrom',
603- absoluteURL(self.context, self.request))
604- return url
605-
606- def setCreator(self, quiz):
607- creator = IPerson(self.request.principal)
608- quiz.editors.add(removeSecurityProxy(creator))
609+ legend = _('Quiz Information')
610+ fields = field.Fields(interfaces.IQuiz['title'])
611+ content_template = ViewPageTemplateFile('templates/form.pt')
612+
613+ def updateActions(self):
614+ super(QuizAddView, self).updateActions()
615+ self.actions['add'].addClass('button-ok')
616+ self.actions['cancel'].addClass('button-cancel')
617+
618+ def create(self, data):
619+ quiz = Quiz()
620+ form.applyChanges(self, quiz, data)
621+ return quiz
622+
623+ def add(self, quiz):
624+ chooser = INameChooser(self.context)
625+ name = chooser.chooseName('', quiz)
626+ self.context[name] = quiz
627+ self.setCreator(quiz)
628+ self._quiz = quiz
629+ return quiz
630+
631+ def nextURL(self):
632+ if self._finishedAdd:
633+ url = absoluteURL(self._quiz, self.request)
634+ else:
635+ url = self.request.get('camefrom',
636+ absoluteURL(self.context, self.request))
637+ return url
638+
639+ def setCreator(self, quiz):
640+ creator = IPerson(self.request.principal)
641+ quiz.editors.add(removeSecurityProxy(creator))
642
643
644 class QuizEditView(flourish.form.Form, form.EditForm):
645
646- legend = _('Quiz Information')
647- fields = field.Fields(interfaces.IQuiz['title'])
648- content_template = ViewPageTemplateFile('templates/form.pt')
649-
650- @property
651- def title(self):
652- return self.context.title
653-
654- def update(self):
655- return form.EditForm.update(self)
656-
657- def updateActions(self):
658- super(QuizEditView, self).updateActions()
659- self.actions['submit'].addClass('button-ok')
660- self.actions['cancel'].addClass('button-cancel')
661-
662- def nextURL(self):
663- return absoluteURL(self.context, self.request)
664-
665- @button.buttonAndHandler(_('Submit'), name='submit')
666- def handle_submit(self, action):
667- super(QuizEditView, self).handleApply.func(self, action)
668- if (self.status == self.successMessage or
669- self.status == self.noChangesMessage):
670- self.request.response.redirect(self.nextURL())
671-
672- @button.buttonAndHandler(_('Cancel'), name='cancel')
673- def handle_cancel(self, action):
674- self.request.response.redirect(self.nextURL())
675+ legend = _('Quiz Information')
676+ fields = field.Fields(interfaces.IQuiz['title'])
677+ content_template = ViewPageTemplateFile('templates/form.pt')
678+
679+ @property
680+ def title(self):
681+ return self.context.title
682+
683+ def update(self):
684+ return form.EditForm.update(self)
685+
686+ def updateActions(self):
687+ super(QuizEditView, self).updateActions()
688+ self.actions['submit'].addClass('button-ok')
689+ self.actions['cancel'].addClass('button-cancel')
690+
691+ def nextURL(self):
692+ return absoluteURL(self.context, self.request)
693+
694+ @button.buttonAndHandler(_('Submit'), name='submit')
695+ def handle_submit(self, action):
696+ super(QuizEditView, self).handleApply.func(self, action)
697+ if (self.status == self.successMessage or
698+ self.status == self.noChangesMessage):
699+ self.request.response.redirect(self.nextURL())
700+
701+ @button.buttonAndHandler(_('Cancel'), name='cancel')
702+ def handle_cancel(self, action):
703+ self.request.response.redirect(self.nextURL())
704
705
706 class QuizEditorsView(EditPersonRelationships):
707
708- current_title = _('Current editors')
709- available_title = _('Add editors')
710-
711- @property
712- def title(self):
713- return self.context.title
714-
715- def getCollection(self):
716- return self.context.editors
717+ current_title = _('Current editors')
718+ available_title = _('Add editors')
719+
720+ @property
721+ def title(self):
722+ return self.context.title
723+
724+ def getCollection(self):
725+ return self.context.editors
726
727
728 class QuizDeleteView(flourish.form.DialogForm):
729
730- template = ViewPageTemplateFile('templates/confirm_delete_quiz.pt')
731-
732- dialog_submit_actions = ('delete',)
733- dialog_close_actions = ('cancel',)
734- label = None
735-
736- @button.buttonAndHandler(_('Delete'), name='delete')
737- def handle_delete(self, action):
738- url = '%s/delete.html?delete.%s&CONFIRM' % (
739- absoluteURL(self.context.__parent__, self.request),
740- self.context.__name__)
741- self.request.response.redirect(url)
742- self.ajax_settings['dialog'] = 'close'
743-
744- @button.buttonAndHandler(_('Cancel'), name='cancel')
745- def handle_cancel(self, action):
746- pass
747-
748- def updateActions(self):
749- super(QuizDeleteView, self).updateActions()
750- self.actions['delete'].addClass('button-ok')
751- self.actions['cancel'].addClass('button-cancel')
752+ template = ViewPageTemplateFile('templates/confirm_delete_quiz.pt')
753+
754+ dialog_submit_actions = ('delete',)
755+ dialog_close_actions = ('cancel',)
756+ label = None
757+
758+ @button.buttonAndHandler(_('Delete'), name='delete')
759+ def handle_delete(self, action):
760+ url = '%s/delete.html?delete.%s&CONFIRM' % (
761+ absoluteURL(self.context.__parent__, self.request),
762+ self.context.__name__)
763+ self.request.response.redirect(url)
764+ self.ajax_settings['dialog'] = 'close'
765+
766+ @button.buttonAndHandler(_('Cancel'), name='cancel')
767+ def handle_cancel(self, action):
768+ pass
769+
770+ def updateActions(self):
771+ super(QuizDeleteView, self).updateActions()
772+ self.actions['delete'].addClass('button-ok')
773+ self.actions['cancel'].addClass('button-cancel')
774
775
776 class QuizDuplicateView(flourish.page.Page):
777
778- def __call__(self):
779- container = self.context.__parent__
780- copy = removeSecurityProxy(self.context).copy()
781- chooser = INameChooser(container)
782- name = chooser.chooseName('', copy)
783- container[name] = copy
784- copy.title = translate(
785- _('DUPLICATED: ${quiz}', mapping={'quiz': self.context.title}),
786- context=self.request)
787- person = IPerson(self.request.principal, None)
788- if person is not None:
789- copy.editors.add(removeSecurityProxy(person))
790- notify(ObjectCreatedEvent(copy))
791- camefrom = self.request.get('camefrom',
792- absoluteURL(container, self.request))
793- self.request.response.redirect(camefrom)
794+ def __call__(self):
795+ container = self.context.__parent__
796+ copy = removeSecurityProxy(self.context).copy()
797+ chooser = INameChooser(container)
798+ name = chooser.chooseName('', copy)
799+ container[name] = copy
800+ copy.title = translate(
801+ _('DUPLICATED: ${quiz}', mapping={'quiz': self.context.title}),
802+ context=self.request)
803+ person = IPerson(self.request.principal, None)
804+ if person is not None:
805+ copy.editors.add(removeSecurityProxy(person))
806+ notify(ObjectCreatedEvent(copy))
807+ camefrom = self.request.get('camefrom',
808+ absoluteURL(container, self.request))
809+ self.request.response.redirect(camefrom)
810
811
812 class QuizContainerDeleteView(flourish.containers.ContainerDeleteView):
813
814- def nextURL(self):
815- if 'CONFIRM' in self.request:
816- person = IPerson(self.request.principal)
817- url = '%s/quizzes.html' % absoluteURL(person, self.request)
818- return url
819- return super(QuizContainerDeleteView, self).nextURL()
820+ def nextURL(self):
821+ if 'CONFIRM' in self.request:
822+ person = IPerson(self.request.principal)
823+ url = '%s/quizzes.html' % absoluteURL(person, self.request)
824+ return url
825+ return super(QuizContainerDeleteView, self).nextURL()
826
827
828 class QuizSolutionView(flourish.page.Page):
829
830- implements(interfaces.ISchoolToolQuizView,
831- interfaces.IMathJaxView)
832-
833- container_class = 'container extra-wide-container'
834-
835- @property
836- def title(self):
837- return self.context.title
838-
839- @Lazy
840- def quiz_items(self):
841- return self.context
842+ implements(interfaces.ISchoolToolQuizView,
843+ interfaces.IMathJaxView)
844+
845+ container_class = 'container extra-wide-container'
846+
847+ @property
848+ def title(self):
849+ return self.context.title
850+
851+ @Lazy
852+ def quiz_items(self):
853+ return self.context
854
855
856 class QuizTitleViewlet(flourish.viewlet.Viewlet):
857
858- template = ViewPageTemplateFile('templates/quiz_title.pt')
859+ template = ViewPageTemplateFile('templates/quiz_title.pt')
860
861
862 class QuizItemsViewlet(flourish.viewlet.Viewlet):
863
864- template = InlineViewPageTemplate('''
865- <tal:block i18n:domain="schooltool.quiz"
866- tal:define="items view/view/quiz_items">
867- <h3 i18n:translate="">Questions</h3>
868- <div tal:condition="items"
869- tal:define="ajax nocall:view/view/providers/ajax"
870- tal:content="structure ajax/view/context/quiz_items_table"
871- />
872- <p i18n:translate="" tal:condition="not:items">
873- This quiz has no questions yet.
874- </p>
875- </tal:block>
876- ''')
877-
878-
879+ template = InlineViewPageTemplate('''
880+ <tal:block i18n:domain="schooltool.quiz"
881+ tal:define="items view/view/quiz_items">
882+ <h3 i18n:translate="">Questions</h3>
883+ <div tal:condition="items"
884+ tal:define="ajax nocall:view/view/providers/ajax"
885+ tal:content="structure ajax/view/context/quiz_items_table"
886+ />
887+ <p i18n:translate="" tal:condition="not:items">
888+ This quiz has no questions yet.
889+ </p>
890+ </tal:block>
891+ ''')
892+
893+
894+<<<<<<< TREE
895+=======
896+class ReStructuredTextColumn(GetterColumn):
897+
898+ def __init__(self, *args, **kw):
899+ self.schema_field = kw.pop('schema_field')
900+ super(ReStructuredTextColumn, self).__init__(*args, **kw)
901+
902+ def renderCell(self, item, formatter):
903+ renderer = flourish.form.Form(item, formatter.request)
904+ renderer.mode = DISPLAY_MODE
905+ renderer.fields = field.Fields(self.schema_field)
906+ renderer.update()
907+ return renderer.widgets['body'].render()
908+
909+
910+class TypeColumn(GetterColumn):
911+
912+ def __init__(self, *args, **kw):
913+ super(TypeColumn, self).__init__(*args, **kw)
914+ factory = getUtility(IVocabularyFactory,
915+ name='schooltool.quiz.quiz_item_types')
916+ vocabulary = factory(None)
917+ self.vocabulary = vocabulary
918+
919+ def getter(self, item, formatter):
920+ term = self.vocabulary.getTerm(item.type_)
921+ return term.title
922+
923+
924+>>>>>>> MERGE-SOURCE
925 class ActionColumn(table.column.ImageInputColumn):
926
927- def __init__(self, name, title=None, action=None, library=None,
928- image=None):
929- super(ActionColumn, self).__init__(
930- 'item.', '', name, library=library, image=image)
931- self.alt = title
932- self.link_title = title
933- self.action = action
934+ def __init__(self, name, title=None, action=None, library=None,
935+ image=None):
936+ super(ActionColumn, self).__init__(
937+ 'item.', '', name, library=library, image=image)
938+ self.alt = title
939+ self.link_title = title
940+ self.action = action
941
942- def template(self):
943- return '\n'.join([
944- '<a href="%(href)s" title="%(title)s">',
945- '<img src="%(src)s" alt="%(alt)s" />',
946- '</a>'
947- ])
948+ def template(self):
949+ return '\n'.join([
950+ '<a href="%(href)s" title="%(title)s">',
951+ '<img src="%(src)s" alt="%(alt)s" />',
952+ '</a>'
953+ ])
954
955
956 class QuizActionColumn(ActionColumn):
957
958- def params(self, item, formatter):
959- result = super(QuizActionColumn, self).params(item, formatter)
960- person_url = absoluteURL(formatter.context, formatter.request)
961- camefrom = '%s/quizzes.html' % person_url
962- quiz_url = absoluteURL(item, formatter.request)
963- result['title'] = translate(self.link_title,
964- context=formatter.request) or ''
965- result['href'] = '%s/%s?camefrom=%s' % (
966- quiz_url, self.action, camefrom)
967- return result
968+ def params(self, item, formatter):
969+ result = super(QuizActionColumn, self).params(item, formatter)
970+ person_url = absoluteURL(formatter.context, formatter.request)
971+ camefrom = '%s/quizzes.html' % person_url
972+ quiz_url = absoluteURL(item, formatter.request)
973+ result['title'] = translate(self.link_title,
974+ context=formatter.request) or ''
975+ result['href'] = '%s/%s?camefrom=%s' % (
976+ quiz_url, self.action, camefrom)
977+ return result
978
979
980 class ItemActionColumn(ActionColumn):
981
982- def params(self, item, formatter):
983- result = super(ItemActionColumn, self).params(item, formatter)
984- quiz_url = absoluteURL(formatter.context, formatter.request)
985- result['title'] = translate(self.link_title,
986- context=formatter.request) or ''
987- result['href'] = '%s/%s?item_id=%s&camefrom=%s' % (
988- quiz_url, self.action, item.__name__, quiz_url)
989- return result
990+ def params(self, item, formatter):
991+ result = super(ItemActionColumn, self).params(item, formatter)
992+ quiz_url = absoluteURL(formatter.context, formatter.request)
993+ result['title'] = translate(self.link_title,
994+ context=formatter.request) or ''
995+ result['href'] = '%s/%s?item_id=%s&camefrom=%s' % (
996+ quiz_url, self.action, item.__name__, quiz_url)
997+ return result
998
999
1000 class ModalItemActionColumn(ItemActionColumn):
1001
1002- template = ViewPageTemplateFile('templates/modal_item_column.pt')
1003+ template = ViewPageTemplateFile('templates/modal_item_column.pt')
1004
1005- def renderCell(self, item, formatter):
1006- params = self.params(item, formatter)
1007- params['dialog_title'] = translate(_('Remove this question?'),
1008- context=formatter.request)
1009- self.context = item
1010- self.request = formatter.request
1011- return self.template(params=params)
1012+ def renderCell(self, item, formatter):
1013+ params = self.params(item, formatter)
1014+ params['dialog_title'] = translate(_('Remove this question?'),
1015+ context=formatter.request)
1016+ self.context = item
1017+ self.request = formatter.request
1018+ return self.template(params=params)
1019
1020
1021 class PositionColumn(GetterColumn):
1022
1023- def getter(self, item, formatter):
1024- for position, current in enumerate(formatter.context):
1025- if sameProxiedObjects(current, item):
1026- return position
1027+ def getter(self, item, formatter):
1028+ for position, current in enumerate(formatter.context):
1029+ if sameProxiedObjects(current, item):
1030+ return position
1031
1032- def cell_formatter(self, value, item, formatter):
1033- template = '<a href="%(href)s">%(title)s</a>'
1034- quiz_url = absoluteURL(formatter.context, formatter.request)
1035- href = '%s/view_item.html?item_id=%s' % (quiz_url, item.__name__)
1036- params = {'href': href, 'title': value + 1}
1037- return template % params
1038+ def cell_formatter(self, value, item, formatter):
1039+ template = '<a href="%(href)s">%(title)s</a>'
1040+ quiz_url = absoluteURL(formatter.context, formatter.request)
1041+ href = '%s/view_item.html?item_id=%s' % (quiz_url, item.__name__)
1042+ params = {'href': href, 'title': value + 1}
1043+ return template % params
1044
1045
1046 class SortColumn(PositionColumn):
1047
1048+<<<<<<< TREE
1049 def cell_formatter(self, value, item, formatter):
1050 template = '<select name="%(name)s" class="item-sort">%(options)s</select>'
1051 options = []
1052@@ -638,10 +793,27 @@
1053 'options': ''.join(options),
1054 }
1055 return template % params
1056+=======
1057+ def cell_formatter(self, value, item, formatter):
1058+ template = '<select id="%(name)s" class="item-sort">%(options)s</select>'
1059+ options = []
1060+ for position, current in enumerate(formatter.context):
1061+ display_position = position + 1
1062+ option = '<option value="%d">%d</option>'
1063+ if sameProxiedObjects(current, item):
1064+ option = '<option value="%d" selected="selected">%d</option>'
1065+ options.append(option % (position, display_position))
1066+ params = {
1067+ 'name': item.__name__,
1068+ 'options': ''.join(options),
1069+ }
1070+ return template % params
1071+>>>>>>> MERGE-SOURCE
1072
1073
1074 class QuizItemsTable(table.ajax.Table):
1075
1076+<<<<<<< TREE
1077 def columns(self):
1078 position = PositionColumn(
1079 name='position',
1080@@ -707,67 +879,137 @@
1081 new_position = int(toChange)
1082 self.context.updateOrder(changePosition, new_position)
1083 super(QuizItemsTable, self).update()
1084+=======
1085+ table_formatter = AJAXCSSTableFormatter
1086+
1087+ def columns(self):
1088+ position = PositionColumn(
1089+ name='position',
1090+ title=_('No.'))
1091+ body = ReStructuredTextColumn(
1092+ name='body',
1093+ title=_('Body'),
1094+ schema_field=interfaces.IQuizItem['body']
1095+ )
1096+ type_ = TypeColumn(
1097+ name='type',
1098+ title=_('Type'))
1099+ result = [position, body, type_]
1100+ result.extend(self.getActionColumns())
1101+ return result
1102+
1103+ def getActionColumns(self):
1104+ # XXX: set action columns only if principal is editor
1105+ sort = SortColumn(
1106+ name='sort',
1107+ title=u'')
1108+ edit = ItemActionColumn(
1109+ 'edit',
1110+ title=_('Edit this item'),
1111+ action='edit_item.html',
1112+ library='schooltool.skin.flourish',
1113+ image='edit-icon.png')
1114+ duplicate = ItemActionColumn(
1115+ 'duplicate',
1116+ title=_('Duplicate this item'),
1117+ action='duplicate_item.html',
1118+ library='schooltool.quiz.flourish',
1119+ image='duplicate-icon.png')
1120+ remove = ModalItemActionColumn(
1121+ 'remove',
1122+ title=_('Remove this item'),
1123+ action='remove_item.html',
1124+ library='schooltool.skin.flourish',
1125+ image='remove-icon.png')
1126+ return [sort, edit, duplicate, remove]
1127+
1128+ def items(self):
1129+ return self.view.quiz_items
1130+
1131+ def sortOn(self):
1132+ return (('position', False),)
1133+
1134+ def updateFormatter(self):
1135+ # XXX: set -with-actions class only if principal is editor
1136+ klass = 'data quiz-items-table quiz-items-table-with-actions'
1137+ css_classes = {'table': klass}
1138+ if self._table_formatter is None:
1139+ self.setUp(table_formatter=self.table_formatter,
1140+ batch_size=self.batch_size,
1141+ prefix=self.__name__,
1142+ css_classes=css_classes)
1143+
1144+ def update(self):
1145+ changePosition = self.request.get('changePosition')
1146+ if changePosition is not None:
1147+ toChange = self.request.get(changePosition)
1148+ if toChange is not None:
1149+ new_position = int(toChange)
1150+ self.context.updateOrder(changePosition, new_position)
1151+ super(QuizItemsTable, self).update()
1152+>>>>>>> MERGE-SOURCE
1153
1154
1155 class QuizViewDoneLinkViewlet(flourish.viewlet.Viewlet):
1156
1157- template = InlineViewPageTemplate('''
1158- <h3 i18n:domain="schooltool" class="done-link">
1159- <a tal:attributes="href view/url"
1160- i18n:translate="">
1161- Done
1162- </a>
1163- </h3>
1164- ''')
1165+ template = InlineViewPageTemplate('''
1166+ <h3 i18n:domain="schooltool" class="done-link">
1167+ <a tal:attributes="href view/url"
1168+ i18n:translate="">
1169+ Done
1170+ </a>
1171+ </h3>
1172+ ''')
1173
1174- def url(self):
1175- person = IPerson(self.request.principal, None)
1176- if person is not None:
1177- return '%s/quizzes.html' % (absoluteURL(person, self.request))
1178+ def url(self):
1179+ person = IPerson(self.request.principal, None)
1180+ if person is not None:
1181+ return '%s/quizzes.html' % (absoluteURL(person, self.request))
1182
1183
1184 class QuizDeleteLinkViewlet(flourish.page.ModalFormLinkViewlet):
1185
1186- @property
1187- def dialog_title(self):
1188- title = _('Delete this quiz?')
1189- return translate(title, context=self.request)
1190+ @property
1191+ def dialog_title(self):
1192+ title = _('Delete this quiz?')
1193+ return translate(title, context=self.request)
1194
1195
1196 class QuizSolutionTableViewlet(flourish.viewlet.Viewlet):
1197
1198- template = InlineViewPageTemplate('''
1199- <div tal:define="ajax nocall:view/view/providers/ajax"
1200- tal:content="structure ajax/view/context/solution_table" />
1201- ''')
1202+ template = InlineViewPageTemplate('''
1203+ <div tal:define="ajax nocall:view/view/providers/ajax"
1204+ tal:content="structure ajax/view/context/solution_table" />
1205+ ''')
1206
1207
1208 class SolutionPositionColumn(PositionColumn):
1209
1210- def cell_formatter(self, value, item, formatter):
1211- return value + 1
1212+ def cell_formatter(self, value, item, formatter):
1213+ return value + 1
1214
1215
1216 def solution_question_formatter(value, item, formatter):
1217- return render_rst(item.body)
1218+ return render_rst(item.body)
1219
1220
1221 def solution_solution_formatter(value, item, formatter):
1222- if item.type_ == u'Open':
1223- return render_rst(item.solution)
1224- choices = []
1225- for choice in item.choices:
1226- body = render_rst(choice.body)
1227- if choice.correct:
1228- choices.append(
1229- '<span class="correct">%s</span>' % body)
1230- else:
1231- choices.append(body)
1232- return ''.join(choices)
1233+ if item.type_ in (u'Open',u'Rational'):
1234+ return render_rst(item.solution)
1235+ choices = []
1236+ for choice in item.choices:
1237+ body = render_rst(choice.body)
1238+ if choice.correct:
1239+ choices.append(
1240+ '<span class="correct">%s</span>' % body)
1241+ else:
1242+ choices.append(body)
1243+ return ''.join(choices)
1244
1245
1246 class SolutionTable(table.ajax.Table):
1247
1248+<<<<<<< TREE
1249 def items(self):
1250 return self.view.quiz_items
1251
1252@@ -799,23 +1041,59 @@
1253 batch_size=self.batch_size,
1254 prefix=self.__name__,
1255 css_classes={'table': 'data solution-table'})
1256+=======
1257+ table_formatter = AJAXCSSTableFormatter
1258+
1259+ def items(self):
1260+ return self.view.quiz_items
1261+
1262+ def columns(self):
1263+ position = SolutionPositionColumn(
1264+ name='position',
1265+ title=_('No.'))
1266+ type_ = TypeColumn(
1267+ name='type',
1268+ title=_('Type'))
1269+ question = GetterColumn(
1270+ name='question',
1271+ title=_('Question'),
1272+ getter=lambda i, f: '',
1273+ cell_formatter=solution_question_formatter)
1274+ solution = GetterColumn(
1275+ name='solution',
1276+ title=_('Solution'),
1277+ getter=lambda i, f: '',
1278+ cell_formatter=solution_solution_formatter)
1279+ return [position, type_, question, solution]
1280+
1281+ def sortOn(self):
1282+ return (('position', False),)
1283+
1284+ def updateFormatter(self):
1285+ if self._table_formatter is None:
1286+ self.setUp(table_formatter=self.table_formatter,
1287+ batch_size=self.batch_size,
1288+ prefix=self.__name__,
1289+ css_classes={'table': 'data solution-table'})
1290+>>>>>>> MERGE-SOURCE
1291
1292
1293 class SolutionDoneLinkViewlet(flourish.viewlet.Viewlet):
1294
1295- template = InlineViewPageTemplate('''
1296- <h3 i18n:domain="schooltool" class="done-link">
1297- <a tal:attributes="href view/url" i18n:translate="">Done</a>
1298- </h3>
1299- ''')
1300+ template = InlineViewPageTemplate('''
1301+ <h3 i18n:domain="schooltool" class="done-link">
1302+ <a tal:attributes="href view/url" i18n:translate="">Done</a>
1303+ </h3>
1304+ ''')
1305
1306- def url(self):
1307- default = absoluteURL(self.context, self.request)
1308- return self.request.get('camefrom', default)
1309+ def url(self):
1310+ default = absoluteURL(self.context, self.request)
1311+ return self.request.get('camefrom', default)
1312
1313
1314 class QuizImporter(ImporterBase):
1315
1316+<<<<<<< TREE
1317 sheet_name = 'QuizItems'
1318
1319 @Lazy
1320@@ -883,126 +1161,196 @@
1321 continue
1322 item = self.createQuizItem(data)
1323 self.addItem(quiz, item, data)
1324+=======
1325+ sheet_name = 'QuizItems'
1326+
1327+ @Lazy
1328+ def types_vocabulary(self):
1329+ factory = getUtility(IVocabularyFactory,
1330+ name='schooltool.quiz.quiz_item_types')
1331+ return factory(None)
1332+
1333+ def createQuiz(self, title):
1334+ quiz = Quiz()
1335+ quiz.title = translate(_('IMPORTED: ${quiz}', mapping={'quiz': title}),
1336+ context=self.request)
1337+ return quiz
1338+
1339+ def createQuizItem(self, data):
1340+ item = QuizItem()
1341+ item.body = data['body']
1342+ item.solution = data['solution']
1343+ item.type_ = self.types_vocabulary.getTermByToken(data['type_']).value
1344+ if data['choices']:
1345+ choices = []
1346+ for choice in data['choices'].split(choices_row_separator):
1347+ body, correct = choice.split(choices_column_separator)
1348+ item_choice = Choice()
1349+ item_choice.body = body
1350+ try:
1351+ item_choice.correct = bool(int(correct))
1352+ except (ValueError,):
1353+ item_choice.correct = False
1354+ choices.append(item_choice)
1355+ item.choices = choices
1356+ return item
1357+
1358+ def addItem(self, quiz, item, data):
1359+ app = ISchoolToolApplication(None)
1360+ container = interfaces.IQuizItemContainer(app)
1361+ chooser = INameChooser(container)
1362+ name = chooser.chooseName('', item)
1363+ container[name] = item
1364+ item.quizzes.add(quiz)
1365+ quiz.order.append(item.__name__)
1366+ notify(ObjectCreatedEvent(item))
1367+
1368+ def process(self):
1369+ sh = self.sheet
1370+ num_errors = len(self.errors)
1371+ title = self.getRequiredTextFromCell(sh, 0, 1)
1372+ if num_errors < len(self.errors):
1373+ return
1374+ quiz = self.createQuiz(title)
1375+ chooser = INameChooser(self.context)
1376+ name = chooser.chooseName('', quiz)
1377+ self.context[name] = quiz
1378+ person = IPerson(self.request.principal)
1379+ quiz.editors.add(removeSecurityProxy(person))
1380+ notify(ObjectCreatedEvent(quiz))
1381+ for row in range(3, sh.nrows):
1382+ num_errors = len(self.errors)
1383+ data = {}
1384+ data['body'] = self.getRequiredTextFromCell(sh, row, 0)
1385+ data['type_'] = self.getRequiredTextFromCell(sh, row, 1)
1386+ data['choices'] = self.getTextFromCell(sh, row, 2)
1387+ data['solution'] = self.getTextFromCell(sh, row, 3)
1388+ if num_errors < len(self.errors):
1389+ continue
1390+ item = self.createQuizItem(data)
1391+ self.addItem(quiz, item, data)
1392+>>>>>>> MERGE-SOURCE
1393
1394
1395 class QuizImportView(FlourishMegaImporter):
1396
1397- content_template = ViewPageTemplateFile('templates/quiz_import.pt')
1398-
1399- @property
1400- def importers(self):
1401- return [
1402- QuizImporter,
1403- ]
1404-
1405- def nextURL(self):
1406- return self.request.get('camefrom',
1407- absoluteURL(self.context, self.request))
1408+ content_template = ViewPageTemplateFile('templates/quiz_import.pt')
1409+
1410+ @property
1411+ def importers(self):
1412+ return [
1413+ QuizImporter,
1414+ ]
1415+
1416+ def nextURL(self):
1417+ return self.request.get('camefrom',
1418+ absoluteURL(self.context, self.request))
1419
1420
1421 class QuizExportView(MegaExporter):
1422
1423- def __call__(self):
1424- wb = xlwt.Workbook()
1425- self.export_items(wb)
1426- datafile = StringIO()
1427- wb.save(datafile)
1428- data = datafile.getvalue()
1429- self.setUpHeaders(data)
1430- return data
1431-
1432- def export_items(self, wb):
1433- ws = wb.add_sheet('QuizItems')
1434- self.write_header(ws, 0, 0, 'Quiz Title')
1435- self.write(ws, 0, 1, self.context.title)
1436- self.print_table(self.format_items(), ws, offset=2)
1437-
1438- def print_table(self, table, ws, offset=0):
1439- for x, row in enumerate(table):
1440- for y, cell in enumerate(row):
1441- self.write(ws, x + offset, y, cell.data, **cell.style)
1442- return len(table)
1443-
1444- def format_items(self):
1445- factory = getUtility(IVocabularyFactory,
1446- name='schooltool.quiz.quiz_item_types')
1447- vocabulary = factory(None)
1448-
1449- def type_getter(item):
1450- return vocabulary.getTerm(item.type_).token
1451-
1452- def choices_getter(item):
1453- result = []
1454- if item.choices is not None:
1455- for choice in item.choices:
1456- result.append(self.format_choice(choice))
1457- return choices_row_separator.join(result)
1458-
1459- fields = [
1460- ('Body', Text, attrgetter('body')),
1461- ('Type', Text, type_getter),
1462- ('Choices', Text, choices_getter),
1463- ('Solution', Text, attrgetter('solution')),
1464- ]
1465- return self.format_table(fields, self.context)
1466-
1467- def format_choice(self, choice):
1468- correct = [0, 1][choice.correct]
1469- return choices_column_separator.join([choice.body, str(correct)])
1470+ def __call__(self):
1471+ wb = xlwt.Workbook()
1472+ self.export_items(wb)
1473+ datafile = StringIO()
1474+ wb.save(datafile)
1475+ data = datafile.getvalue()
1476+ self.setUpHeaders(data)
1477+ return data
1478+
1479+ def export_items(self, wb):
1480+ ws = wb.add_sheet('QuizItems')
1481+ self.write_header(ws, 0, 0, 'Quiz Title')
1482+ self.write(ws, 0, 1, self.context.title)
1483+ self.print_table(self.format_items(), ws, offset=2)
1484+
1485+ def print_table(self, table, ws, offset=0):
1486+ for x, row in enumerate(table):
1487+ for y, cell in enumerate(row):
1488+ self.write(ws, x + offset, y, cell.data, **cell.style)
1489+ return len(table)
1490+
1491+ def format_items(self):
1492+ factory = getUtility(IVocabularyFactory,
1493+ name='schooltool.quiz.quiz_item_types')
1494+ vocabulary = factory(None)
1495+
1496+ def type_getter(item):
1497+ return vocabulary.getTerm(item.type_).token
1498+
1499+ def choices_getter(item):
1500+ result = []
1501+ if item.choices is not None:
1502+ for choice in item.choices:
1503+ result.append(self.format_choice(choice))
1504+ return choices_row_separator.join(result)
1505+
1506+ fields = [
1507+ ('Body', Text, attrgetter('body')),
1508+ ('Type', Text, type_getter),
1509+ ('Choices', Text, choices_getter),
1510+ ('Solution', Text, attrgetter('solution')),
1511+ ]
1512+ return self.format_table(fields, self.context)
1513+
1514+ def format_choice(self, choice):
1515+ correct = [0, 1][choice.correct]
1516+ return choices_column_separator.join([choice.body, str(correct)])
1517
1518
1519 class QuizHelpLinks(flourish.page.RefineLinksViewlet, PersonSections):
1520
1521- @Lazy
1522- def person(self):
1523- return IPerson(self.request.principal, None)
1524-
1525- @property
1526- def enabled(self):
1527- if self.person is not None:
1528- editable_quizzes = getRelatedObjects(self.person,
1529- relationships.URIQuiz,
1530- relationships.URIQuizEditors)
1531- return self.instructor_sections or editable_quizzes
1532-
1533- def render(self, *args, **kw):
1534- if self.enabled:
1535- return super(QuizHelpLinks, self).render(*args, **kw)
1536- return ''
1537+ @Lazy
1538+ def person(self):
1539+ return IPerson(self.request.principal, None)
1540+
1541+ @property
1542+ def enabled(self):
1543+ if self.person is not None:
1544+ editable_quizzes = getRelatedObjects(self.person,
1545+ relationships.URIQuiz,
1546+ relationships.URIQuizEditors)
1547+ return self.instructor_sections or editable_quizzes
1548+
1549+ def render(self, *args, **kw):
1550+ if self.enabled:
1551+ return super(QuizHelpLinks, self).render(*args, **kw)
1552+ return ''
1553
1554
1555 class QuizEditingHelpViewlet(flourish.page.LinkIdViewlet):
1556
1557- template = InlineViewPageTemplate('''
1558- <a tal:attributes="href view/url;
1559- onclick view/script"
1560- tal:content="view/title" />
1561- ''')
1562-
1563- @property
1564- def enabled(self):
1565- app = ISchoolToolApplication(None)
1566- preferences = interfaces.IApplicationPreferences(app)
1567- return preferences.mathjax_help is not None
1568-
1569- @property
1570- def script(self):
1571- url = self.url
1572- name = self.html_id
1573- params = {
1574- 'width': 640,
1575- 'height': 480,
1576- 'resizable': 'yes',
1577- 'scrollbars': 'yes',
1578- 'toolbar': 'no',
1579- 'location': 'no',
1580- }
1581- return "window.open('%s', '%s', '%s'); return false" % (
1582- url, name, ','.join(['%s=%s' % (k, v) for k, v in params.items()]))
1583+ template = InlineViewPageTemplate('''
1584+ <a tal:attributes="href view/url;
1585+ onclick view/script"
1586+ tal:content="view/title" />
1587+ ''')
1588+
1589+ @property
1590+ def enabled(self):
1591+ app = ISchoolToolApplication(None)
1592+ preferences = interfaces.IApplicationPreferences(app)
1593+ return preferences.mathjax_help is not None
1594+
1595+ @property
1596+ def script(self):
1597+ url = self.url
1598+ name = self.html_id
1599+ params = {
1600+ 'width': 640,
1601+ 'height': 480,
1602+ 'resizable': 'yes',
1603+ 'scrollbars': 'yes',
1604+ 'toolbar': 'no',
1605+ 'location': 'no',
1606+ }
1607+ return "window.open('%s', '%s', '%s'); return false" % (
1608+ url, name, ','.join(['%s=%s' % (k, v) for k, v in params.items()]))
1609
1610
1611 class QuizEditingHelpView(flourish.page.Page):
1612
1613+<<<<<<< TREE
1614 implements(interfaces.ISchoolToolQuizView,
1615 interfaces.IMathJaxView)
1616
1617@@ -1032,3 +1380,25 @@
1618 MathJax.Hub.Queue(['Typeset', MathJax.Hub]);
1619 </script>
1620 ''')
1621+=======
1622+ implements(interfaces.ISchoolToolQuizView,
1623+ interfaces.IMathJaxView)
1624+
1625+ template = InlineViewPageTemplate('''
1626+ <tal:block content="structure view/mathjax_help" />
1627+ ''')
1628+
1629+ def mathjax_help(self):
1630+ app = ISchoolToolApplication(None)
1631+ preferences = interfaces.IApplicationPreferences(app)
1632+ return self.render_rst(preferences,
1633+ interfaces.IApplicationPreferences,
1634+ 'mathjax_help')
1635+
1636+ def render_rst(self, value, iface, field_name):
1637+ renderer = flourish.form.Form(value, self.request)
1638+ renderer.mode = DISPLAY_MODE
1639+ renderer.fields = field.Fields(iface[field_name])
1640+ renderer.update()
1641+ return renderer.widgets[field_name].render()
1642+>>>>>>> MERGE-SOURCE
1643
1644=== modified file 'src/schooltool/quiz/browser/quizitem.py'
1645--- src/schooltool/quiz/browser/quizitem.py 2013-01-24 06:54:46 +0000
1646+++ src/schooltool/quiz/browser/quizitem.py 2013-03-04 22:05:25 +0000
1647@@ -59,6 +59,7 @@
1648 from schooltool.quiz.browser.widget import LabeledCheckBoxFieldWidget
1649 from schooltool.quiz.quizitem import Choice
1650 from schooltool.quiz.quizitem import QuizItem
1651+from schooltool.quiz.utils import Validator
1652
1653 registerFactoryAdapter(interfaces.IChoice, Choice)
1654
1655@@ -66,6 +67,10 @@
1656 class OneCorrectChoiceRequired(ValidationError):
1657
1658 __doc__ = _('One correct choice is required.')
1659+
1660+class ValidSolutionRequired(ValidationError):
1661+
1662+ __doc__ = _('A syntactically valid solution is required.')
1663
1664
1665 class SolutionRequiredError(ValidationError):
1666@@ -83,7 +88,8 @@
1667
1668 def validate(self, value):
1669 super(ChoicesValidator, self).validate(value)
1670- if not self.view.is_open_question():
1671+ if not (self.view.is_open_question() \
1672+ or self.view.is_rational_question()):
1673 if not value:
1674 raise OneCorrectChoiceRequired(value)
1675 correct = []
1676@@ -98,8 +104,12 @@
1677
1678 def validate(self, value):
1679 super(SolutionValidator, self).validate(value)
1680- if self.view.is_open_question() and not value:
1681+ if (self.view.is_open_question() \
1682+ or self.view.is_rational_question()) \
1683+ and not value:
1684 raise SolutionRequiredError(value)
1685+ if self.view.is_rational_question() and not Validator(value):
1686+ raise ValidSolutionRequired(value)
1687
1688
1689 class ChoiceSubForm(ObjectSubForm):
1690@@ -118,6 +128,7 @@
1691 container_class = 'container widecontainer'
1692 content_template = ViewPageTemplateFile('templates/form.pt')
1693 _open_question_token = 'open'
1694+ _rational_question_token = 'rational'
1695
1696 @property
1697 def fields(self):
1698@@ -146,7 +157,7 @@
1699 result = {}
1700 for widget in self.widgets.values():
1701 result.update(self.encodeWidget(widget))
1702- if self.is_open_question():
1703+ if self.is_open_question() or self.is_rational_question():
1704 solution_widget = self.widgets['solution']
1705 result.update(self.encodeWidget(solution_widget))
1706 else:
1707@@ -162,6 +173,10 @@
1708 def is_open_question(self):
1709 item_type = getattr(self.widgets['type_'], 'value', [])
1710 return self._open_question_token in item_type
1711+
1712+ def is_rational_question(self):
1713+ item_type = getattr(self.widgets['type_'], 'value', [])
1714+ return self._rational_question_token in item_type
1715
1716 def setQuiz(self, item):
1717 quiz = removeSecurityProxy(self.context)
1718@@ -419,6 +434,7 @@
1719 'selection': _('Selection Question'),
1720 'multiple': _('Multiple Selection Question'),
1721 'open': _('Open Question'),
1722+ 'rational': _('Rational Question'),
1723 }
1724 return result[self.type_token]
1725
1726@@ -431,6 +447,7 @@
1727 'selection': _('Selection Question'),
1728 'multiple': _('Multiple Selection Question'),
1729 'open': _('Open Question'),
1730+ 'rational': _('Rational Question'),
1731 }
1732 return result[self.type_token]
1733
1734@@ -556,7 +573,13 @@
1735
1736 def is_open_question(self):
1737 return self.view.type_token == 'open'
1738+
1739+ def is_rational_question(self):
1740+ return self.view.type_token == 'rational'
1741
1742+ def has_open_field(self):
1743+ return self.is_rational_question() or self.is_open_question()
1744+
1745 def has_skill(self):
1746 return None not in (self.view.item.course, self.view.item.skill)
1747
1748
1749=== modified file 'src/schooltool/quiz/browser/stests/quiz_deployment.txt'
1750--- src/schooltool/quiz/browser/stests/quiz_deployment.txt 2012-10-07 06:00:01 +0000
1751+++ src/schooltool/quiz/browser/stests/quiz_deployment.txt 2013-03-04 22:05:25 +0000
1752@@ -64,6 +64,11 @@
1753 ... ('Eric Raymond', False),
1754 ... ]
1755 >>> fill_question_form(teacher, 'Selection', body, choices)
1756+ >>> teacher.query.id('form-buttons-submitadd').click()
1757+
1758+ >>> body = 'What is the first prime number higher than 5?'
1759+ >>> solution = '3 + 4'
1760+ >>> fill_question_form(teacher, 'Rational', body, solution=solution)
1761 >>> teacher.query.id('form-buttons-submit').click()
1762
1763 >>> print_quiz_item_bodies(teacher)
1764@@ -82,6 +87,11 @@
1765 Who is Python's original author?
1766 </p>
1767 </div>
1768+ <div ...>
1769+ <p>
1770+ What is the first prime number higher than 5?
1771+ </p>
1772+ </div>
1773
1774 Deploy the quiz to one of the instructor's sections:
1775
1776@@ -162,6 +172,9 @@
1777 >>> sel = '#form-widgets-QuizItem-3-row .radio-widget[value="4"]'
1778 >>> camila.query.css(sel).click()
1779
1780+ >>> answer_4 = '9/3 + 4'
1781+ >>> camila.query.id('form-widgets-QuizItem-4').ui.set_value(answer_4)
1782+
1783 >>> camila.query.id('form-buttons-submit').click()
1784
1785 >>> sel = '.taken-quizzes-table tbody tr'
1786@@ -189,6 +202,9 @@
1787 >>> sel = '#form-widgets-QuizItem-3-row .radio-widget[value="3"]'
1788 >>> mario.query.css(sel).click()
1789
1790+ >>> answer_4 = '6'
1791+ >>> mario.query.id('form-widgets-QuizItem-4').ui.set_value(answer_4)
1792+
1793 >>> mario.query.id('form-buttons-submit').click()
1794
1795 >>> sel = '.taken-quizzes-table tbody tr'
1796@@ -216,6 +232,9 @@
1797 >>> sel = '#form-widgets-QuizItem-3-row .radio-widget[value="3"]'
1798 >>> liliana.query.css(sel).click()
1799
1800+ >>> answer_4 = '7'
1801+ >>> liliana.query.id('form-widgets-QuizItem-4').ui.set_value(answer_4)
1802+
1803 >>> liliana.query.id('form-buttons-submit').click()
1804
1805 >>> sel = '.taken-quizzes-table tbody tr'
1806@@ -238,7 +257,7 @@
1807 The instructor updates the grades:
1808
1809 >>> teacher.query.link('Update Grades').click()
1810-
1811+
1812 The instructor goes to see the gradebook:
1813
1814 >>> teacher.ui.section.go('2012', '2012', 'Programming A1')
1815@@ -252,9 +271,9 @@
1816 | Name | FreeS | Total | Ave. |
1817 | | 100 | | |
1818 +------------------+-------+-------+-------+
1819- | Cerna, Camila | 94.4 | 94.4 | 94.4% |
1820- | Tejada, Mario | 16.7 | 16.7 | 16.7% |
1821- | Vividor, Liliana | 61.1 | 61.1 | 61.1% |
1822+ | Cerna, Camila | 95.8 | 95.8 | 95.8% |
1823+ | Tejada, Mario | 12.5 | 12.5 | 12.5% |
1824+ | Vividor, Liliana | 70.8 | 70.8 | 70.8% |
1825 +------------------+-------+-------+-------+
1826
1827 Camila checks her grade:
1828@@ -264,7 +283,7 @@
1829 >>> for row in camila.query_all.css(sel):
1830 ... columns = row.query_all.tag('td')
1831 ... print ' | '.join([column.text for column in columns])
1832- Free Software Exam | Programming A1 | | 94%
1833+ Free Software Exam | Programming A1 | | 96%
1834
1835 Mario checks his grade:
1836
1837@@ -273,7 +292,7 @@
1838 >>> for row in mario.query_all.css(sel):
1839 ... columns = row.query_all.tag('td')
1840 ... print ' | '.join([column.text for column in columns])
1841- Free Software Exam | Programming A1 | | 17%
1842+ Free Software Exam | Programming A1 | | 12%
1843
1844 Liliana checks her grade:
1845
1846@@ -282,4 +301,4 @@
1847 >>> for row in liliana.query_all.css(sel):
1848 ... columns = row.query_all.tag('td')
1849 ... print ' | '.join([column.text for column in columns])
1850- Free Software Exam | Programming A1 | | 61%
1851+ Free Software Exam | Programming A1 | | 71%
1852
1853=== modified file 'src/schooltool/quiz/browser/stests/quiz_duplicate.txt'
1854--- src/schooltool/quiz/browser/stests/quiz_duplicate.txt 2012-12-06 18:26:06 +0000
1855+++ src/schooltool/quiz/browser/stests/quiz_duplicate.txt 2013-03-04 22:05:25 +0000
1856@@ -59,6 +59,11 @@
1857 ... ('Eric Raymond', False),
1858 ... ]
1859 >>> fill_question_form(teacher, 'Selection', body, choices)
1860+ >>> teacher.query.id('form-buttons-submitadd').click()
1861+
1862+ >>> body = 'What is the first prime number higher than 5?'
1863+ >>> solution = '3 + 4'
1864+ >>> fill_question_form(teacher, 'Rational', body, solution=solution)
1865 >>> teacher.query.id('form-buttons-submit').click()
1866
1867 Check the quiz:
1868@@ -68,7 +73,7 @@
1869 >>> for row in teacher.query_all.css(sel):
1870 ... columns = row.query_all.tag('td')
1871 ... print ' | '.join([column.text for column in columns])
1872- Free/Open Source Software Quiz | 3 | Elkner, Jeffrey | ...
1873+ Free/Open Source Software Quiz | 4 | Elkner, Jeffrey | ...
1874
1875 Duplicate the quiz:
1876
1877@@ -81,8 +86,8 @@
1878 >>> for row in teacher.query_all.css(sel):
1879 ... columns = row.query_all.tag('td')
1880 ... print ' | '.join([column.text for column in columns])
1881- DUPLICATED: Free/Open Source Software Quiz | 3 | Elkner, Jeffrey | ...
1882- Free/Open Source Software Quiz | 3 | Elkner, Jeffrey | ...
1883+ DUPLICATED: Free/Open Source Software Quiz | 4 | Elkner, Jeffrey | ...
1884+ Free/Open Source Software Quiz | 4 | Elkner, Jeffrey | ...
1885
1886 >>> teacher.query.link('DUPLICATED: Free/Open Source Software Quiz').click()
1887 >>> print_quiz_item_bodies(teacher)
1888@@ -101,8 +106,14 @@
1889 Who is Python's original author?
1890 </p>
1891 </div>
1892+ <div ...>
1893+ <p>
1894+ What is the first prime number higher than 5?
1895+ </p>
1896+ </div>
1897
1898 >>> print_quiz_item_types(teacher)
1899 Open
1900 Multiple Selection
1901- Selection
1902+ Selection
1903+ Rational
1904
1905=== modified file 'src/schooltool/quiz/browser/stests/quiz_export.txt'
1906--- src/schooltool/quiz/browser/stests/quiz_export.txt 2012-10-10 23:31:01 +0000
1907+++ src/schooltool/quiz/browser/stests/quiz_export.txt 2013-03-04 22:05:25 +0000
1908@@ -59,6 +59,11 @@
1909 ... ('Eric Raymond', False),
1910 ... ]
1911 >>> fill_question_form(teacher, 'Selection', body, choices)
1912+ >>> teacher.query.id('form-buttons-submitadd').click()
1913+
1914+ >>> body = 'What is the first prime number higher than 5?'
1915+ >>> solution = '3 + 4'
1916+ >>> fill_question_form(teacher, 'Rational', body, solution=solution)
1917 >>> teacher.query.id('form-buttons-submit').click()
1918
1919 Export the quiz:
1920
1921=== modified file 'src/schooltool/quiz/browser/stests/quiz_import.txt'
1922--- src/schooltool/quiz/browser/stests/quiz_import.txt 2012-12-06 18:26:06 +0000
1923+++ src/schooltool/quiz/browser/stests/quiz_import.txt 2013-03-04 22:05:25 +0000
1924@@ -35,7 +35,7 @@
1925 >>> for row in teacher.query_all.css(sel):
1926 ... columns = row.query_all.tag('td')
1927 ... print ' | '.join([column.text for column in columns])
1928- IMPORTED: Prime Factors 1 | 9 | Elkner, Jeffrey | ...
1929+ IMPORTED: Prime Factors 1 | 11 | Elkner, Jeffrey | ...
1930
1931 >>> teacher.query.link('IMPORTED: Prime Factors 1').click()
1932 >>> print teacher.query.css('.page .header h1').text
1933@@ -69,3 +69,9 @@
1934 <div class="textarea-widget ...">
1935 ...
1936 </div>
1937+ <div class="textarea-widget ...">
1938+ ...
1939+ </div>
1940+ <div class="textarea-widget ...">
1941+ ...
1942+ </div>
1943
1944=== modified file 'src/schooltool/quiz/browser/stests/quiz_import.xls'
1945Binary files src/schooltool/quiz/browser/stests/quiz_import.xls 2012-10-10 23:31:01 +0000 and src/schooltool/quiz/browser/stests/quiz_import.xls 2013-03-04 22:05:25 +0000 differ
1946=== modified file 'src/schooltool/quiz/browser/stests/quiz_management.txt'
1947--- src/schooltool/quiz/browser/stests/quiz_management.txt 2012-09-06 17:35:42 +0000
1948+++ src/schooltool/quiz/browser/stests/quiz_management.txt 2013-03-04 22:05:25 +0000
1949@@ -138,6 +138,7 @@
1950 Selection
1951 Multiple Selection
1952 Open
1953+ Rational
1954
1955 With Selection being selected by default:
1956
1957@@ -319,8 +320,72 @@
1958 Which of the following are free software?
1959 </p>
1960 </div>
1961-
1962-Let's add one more question:
1963+
1964+And this time let's try a rational question. We'll try two examples
1965+of invalid solutions, to test the validator.
1966+
1967+ >>> teacher.query.link('Question').click()
1968+ >>> sel = '//span[@class="label" and text()="Rational"]'
1969+ >>> teacher.query.xpath(sel).click()
1970+
1971+It has a solution, not choices
1972+
1973+ >>> teacher.query.id('form-widgets-solution').is_displayed()
1974+ True
1975+
1976+An empty solution gives an error:
1977+
1978+ >>> body = 'What is the first prime number higher than 5?'
1979+ >>> solution = ''
1980+ >>> fill_question_form(teacher, 'Rational', body, solution=solution)
1981+ >>> teacher.query.id('form-buttons-submit').click()
1982+
1983+ >>> sel = '#form-widgets-solution-row .error div.error'
1984+ >>> print teacher.query.css(sel).text
1985+ Required input is missing.
1986+
1987+So does an unformatted one:
1988+
1989+ >>> body = 'What is the first prime number higher than 5?'
1990+ >>> solution = '3+4'
1991+ >>> fill_question_form(teacher, 'Rational', body, solution=solution)
1992+ >>> teacher.query.id('form-buttons-submit').click()
1993+
1994+ >>> sel = '#form-widgets-solution-row .error div.error'
1995+ >>> print teacher.query.css(sel).text
1996+ A syntactically valid solution is required.
1997+
1998+So let's close this, open a new one, and submit that:
1999+
2000+ >>> teacher.query.id('form-buttons-cancel').click()
2001+ >>> teacher.query.link('Question').click()
2002+
2003+ >>> body = 'What is the first prime number higher than 5?'
2004+ >>> solution = '3 + 4'
2005+ >>> fill_question_form(teacher, 'Rational', body, solution=solution)
2006+ >>> teacher.query.id('form-buttons-submit').click()
2007+
2008+Now the rational question should also be visible:
2009+
2010+ >>> print_quiz_item_bodies(teacher)
2011+ <div ...>
2012+ <p>
2013+ What is the meaning of RMS?
2014+ </p>
2015+ </div>
2016+ <div ...>
2017+ <p>
2018+ Which of the following are free software?
2019+ </p>
2020+ </div>
2021+ <div ...>
2022+ <p>
2023+ What is the first prime number higher than 5?
2024+ </p>
2025+ </div>
2026+
2027+
2028+And just one more:
2029
2030 >>> teacher.query.link('Question').click()
2031 >>> body = "Who is Python's original author?"
2032@@ -378,6 +443,11 @@
2033 </div>
2034 <div ...>
2035 <p>
2036+ What is the first prime number higher than 5?
2037+ </p>
2038+ </div>
2039+ <div ...>
2040+ <p>
2041 Who is Python's original author?
2042 </p>
2043 </div>
2044@@ -385,9 +455,10 @@
2045 >>> print_quiz_item_types(teacher)
2046 Open
2047 Multiple Selection
2048+ Rational
2049 Selection
2050
2051-Now, let's visit eache of these questions, to see their content. To do
2052+Now, let's visit each of these questions, to see their content. To do
2053 so, we click the link in the No. column:
2054
2055 >>> teacher.query.link('1').click()
2056@@ -488,6 +559,34 @@
2057 >>> teacher.query.link('Done').click()
2058
2059 >>> teacher.query.link('3').click()
2060+
2061+ >>> print teacher.query.css('.page .header h1').text
2062+ Free/Open Source Software Quiz
2063+
2064+ >>> print teacher.query.css('.page .header h2').text
2065+ Rational Question
2066+
2067+ >>> sel = '.quiz-item-body .body .restructuredtext-widget'
2068+ >>> for content in teacher.query_all.css(sel):
2069+ ... print content
2070+ <div ...>
2071+ <p>
2072+ What is the first prime number higher than 5?
2073+ </p>
2074+ </div>
2075+
2076+ >>> sel = '.quiz-item-body .solution .restructuredtext-widget'
2077+ >>> for content in teacher.query_all.css(sel):
2078+ ... print content
2079+ <div ...>
2080+ <p>
2081+ 3 + 4
2082+ </p>
2083+ </div>
2084+
2085+ >>> teacher.query.link('Done').click()
2086+
2087+ >>> teacher.query.link('4').click()
2088
2089 >>> print teacher.query.css('.page .header h1').text
2090 Free/Open Source Software Quiz
2091@@ -581,6 +680,11 @@
2092 </div>
2093 <div ...>
2094 <p>
2095+ What is the first prime number higher than 5?
2096+ </p>
2097+ </div>
2098+ <div ...>
2099+ <p>
2100 Who is Python's original author?
2101 </p>
2102 </div>
2103@@ -593,6 +697,7 @@
2104 >>> print_quiz_item_types(teacher)
2105 Open
2106 Multiple Selection
2107+ Rational
2108 Selection
2109 Multiple Selection
2110
2111@@ -618,6 +723,11 @@
2112 </div>
2113 <div ...>
2114 <p>
2115+ What is the first prime number higher than 5?
2116+ </p>
2117+ </div>
2118+ <div ...>
2119+ <p>
2120 Who is Python's original author?
2121 </p>
2122 </div>
2123@@ -629,6 +739,7 @@
2124
2125 >>> print_quiz_item_types(teacher)
2126 Multiple Selection
2127+ Rational
2128 Selection
2129 Multiple Selection
2130
2131
2132=== modified file 'src/schooltool/quiz/browser/stests/student_answers.txt'
2133--- src/schooltool/quiz/browser/stests/student_answers.txt 2012-10-07 17:40:13 +0000
2134+++ src/schooltool/quiz/browser/stests/student_answers.txt 2013-03-04 22:05:25 +0000
2135@@ -64,6 +64,11 @@
2136 ... ('Eric Raymond', False),
2137 ... ]
2138 >>> fill_question_form(teacher, 'Selection', body, choices)
2139+ >>> teacher.query.id('form-buttons-submitadd').click()
2140+
2141+ >>> body = 'What is the first prime number higher than 5?'
2142+ >>> solution = '3 + 4'
2143+ >>> fill_question_form(teacher, 'Rational', body, solution=solution)
2144 >>> teacher.query.id('form-buttons-submit').click()
2145
2146 Deploy the quiz to one of the instructor's sections:
2147@@ -123,6 +128,9 @@
2148
2149 >>> sel = '#form-widgets-QuizItem-3-row .radio-widget[value="4"]'
2150 >>> camila.query.css(sel).click()
2151+
2152+ >>> answer_4 = '9/3 + 4'
2153+ >>> camila.query.id('form-widgets-QuizItem-4').ui.set_value(answer_4)
2154
2155 >>> camila.query.id('form-buttons-submit').click()
2156
2157@@ -151,6 +159,9 @@
2158 >>> sel = '#form-widgets-QuizItem-3-row .radio-widget[value="3"]'
2159 >>> mario.query.css(sel).click()
2160
2161+ >>> answer_4 = '6'
2162+ >>> mario.query.id('form-widgets-QuizItem-4').ui.set_value(answer_4)
2163+
2164 >>> mario.query.id('form-buttons-submit').click()
2165
2166 >>> sel = '.taken-quizzes-table tbody tr'
2167@@ -178,6 +189,9 @@
2168 >>> sel = '#form-widgets-QuizItem-3-row .radio-widget[value="3"]'
2169 >>> liliana.query.css(sel).click()
2170
2171+ >>> answer_4 = '7'
2172+ >>> liliana.query.id('form-widgets-QuizItem-4').ui.set_value(answer_4)
2173+
2174 >>> liliana.query.id('form-buttons-submit').click()
2175
2176 >>> sel = '.taken-quizzes-table tbody tr'
2177@@ -262,3 +276,18 @@
2178 <table ...>
2179 </table>
2180 </div>
2181+ Question 4
2182+ <div ...>
2183+ ...
2184+ <p>
2185+ What is the first prime number higher than 5?
2186+ </p>
2187+ ...
2188+ </div>
2189+ <div ...>
2190+ ...
2191+ <pre>
2192+ 9/3 + 4
2193+ </pre>
2194+ ...
2195+ </div>
2196
2197=== modified file 'src/schooltool/quiz/browser/stests/test_selenium.py'
2198--- src/schooltool/quiz/browser/stests/test_selenium.py 2012-12-02 07:10:04 +0000
2199+++ src/schooltool/quiz/browser/stests/test_selenium.py 2013-03-04 22:05:25 +0000
2200@@ -56,7 +56,7 @@
2201 sel = '//span[@class="label" and text()="%s"]' % type_
2202 browser.query.xpath(sel).click()
2203 browser.query.id('form-widgets-body').ui.set_value(body)
2204- if type_ == 'Open':
2205+ if type_ in ('Open','Rational'):
2206 sel = 'form-widgets-solution'
2207 browser.query.id(sel).ui.set_value(solution)
2208 else:
2209
2210=== modified file 'src/schooltool/quiz/browser/templates/answers.pt'
2211--- src/schooltool/quiz/browser/templates/answers.pt 2012-11-29 15:28:05 +0000
2212+++ src/schooltool/quiz/browser/templates/answers.pt 2013-03-04 22:05:25 +0000
2213@@ -7,7 +7,7 @@
2214 <div class="evaluation"
2215 tal:define="item evaluation/requirement;
2216 answer evaluation/answer;
2217- is_open_question python:view.is_open(item)">
2218+ has_open_view python:view.has_open_view(item);">
2219 <h3 class="number" i18n:translate=""
2220 style="border-top: 1px solid #000; border-bottom: 1px solid #000; padding: 4px 0;">
2221 Question
2222@@ -22,13 +22,13 @@
2223 Your answer
2224 </h3>
2225 <div class="answer">
2226- <div tal:condition="is_open_question"
2227+ <div tal:condition="has_open_view"
2228 tal:define="score_info python:view.score_info(evaluation)"
2229 tal:attributes="class score_info/css">
2230 <h3 class="score-type" tal:content="score_info/label" />
2231 <div tal:content="structure python:view.render_pre(answer)" />
2232 </div>
2233- <table class="data" tal:condition="not:is_open_question">
2234+ <table class="data" tal:condition="not:has_open_view">
2235 <thead>
2236 <th i18n:translate="">Choice</th>
2237 <th i18n:translate="">Correct Answers</th>
2238@@ -61,7 +61,7 @@
2239 </table>
2240 </div>
2241 <tal:block define="graded python:view.is_graded(evaluation)"
2242- condition="python:is_open_question and graded">
2243+ condition="python:has_open_view and graded">
2244 <h3 i18n:translate="">
2245 Solution
2246 </h3>
2247
2248=== modified file 'src/schooltool/quiz/browser/templates/quiz_item_body.pt'
2249--- src/schooltool/quiz/browser/templates/quiz_item_body.pt 2012-10-25 20:31:25 +0000
2250+++ src/schooltool/quiz/browser/templates/quiz_item_body.pt 2013-03-04 22:05:25 +0000
2251@@ -1,5 +1,5 @@
2252 <tal:block i18n:domain="schooltool.quiz"
2253- define="is_open_question view/is_open_question">
2254+ define="has_open_field view/has_open_field">
2255 <table class="quiz-item-body">
2256 <thead>
2257 <tr>
2258@@ -12,7 +12,8 @@
2259 </tr>
2260 </tbody>
2261 </table>
2262- <table class="data quiz-item-body" tal:condition="is_open_question">
2263+ <table class="data quiz-item-body"
2264+ tal:condition="has_open_field">
2265 <thead>
2266 <tr>
2267 <th i18n:translate="">Solution</th>
2268@@ -25,7 +26,8 @@
2269 </tbody>
2270 </table>
2271 <table
2272- class="data quiz-item-body" tal:condition="not:is_open_question"
2273+ class="data quiz-item-body"
2274+ tal:condition="not:has_open_field"
2275 tal:define="flourish nocall:context/++resource++schooltool.quiz.flourish;
2276 correct_icon flourish/correct-icon.png;
2277 error_icon flourish/error-icon.png;">
2278
2279=== modified file 'src/schooltool/quiz/browser/templates/quiz_item_form_script.pt'
2280--- src/schooltool/quiz/browser/templates/quiz_item_form_script.pt 2012-10-25 20:31:25 +0000
2281+++ src/schooltool/quiz/browser/templates/quiz_item_form_script.pt 2013-03-04 22:05:25 +0000
2282@@ -2,6 +2,7 @@
2283 <tal:script
2284 tal:define="widgets view/view/widgets;
2285 open_question_token view/view/_open_question_token;
2286+ rational_question_token view/view/_rational_question_token;
2287 course nocall:widgets/course|nothing;
2288 skill nocall:widgets/skill|nothing;"
2289 tal:replace="structure scriptlocal:
2290@@ -9,6 +10,7 @@
2291 choices_id widgets/choices/id;
2292 solution_id widgets/solution/id;
2293 open_question_token;
2294+ rational_question_token;
2295 course_id course/id|nothing;
2296 course_novalue_token course/noValueToken|nothing;
2297 skill_id skill/id|nothing;
2298@@ -24,6 +26,7 @@
2299 skill_id = ST.local.skill_id,
2300 skill_novalue_token = ST.local.skill_novalue_token,
2301 open_question_token = ST.local.open_question_token;
2302+ rational_question_token = ST.local.rational_question_token;
2303 return function(e) {
2304 var item_types = $('input[id^="'+type_id+'"]'),
2305 course,
2306@@ -66,7 +69,8 @@
2307 var type_value = $(this).attr('value'),
2308 choices_row = $('.row[id^="'+choices_id+'"]'),
2309 solution_row = $('.row[id^="'+solution_id+'"]');
2310- if (type_value == open_question_token) {
2311+ if ((type_value == open_question_token)
2312+ || (type_value == rational_question_token)){
2313 solution_row.show();
2314 choices_row.hide();
2315 } else {
2316
2317=== modified file 'src/schooltool/quiz/quiz.py'
2318--- src/schooltool/quiz/quiz.py 2013-01-10 07:34:28 +0000
2319+++ src/schooltool/quiz/quiz.py 2013-03-04 22:05:25 +0000
2320@@ -78,7 +78,12 @@
2321 name = chooser.chooseName('', copy)
2322 item_container[name] = copy
2323 result.items.add(copy)
2324+<<<<<<< TREE
2325 result.order.append(name)
2326+=======
2327+ for item_id in self.order:
2328+ result.order.append(item_id) #formerly copy.order...
2329+>>>>>>> MERGE-SOURCE
2330 return result
2331
2332 def updateOrder(self, item_id, position):
2333
2334=== modified file 'src/schooltool/quiz/quizitem.py'
2335--- src/schooltool/quiz/quizitem.py 2013-01-17 08:12:40 +0000
2336+++ src/schooltool/quiz/quizitem.py 2013-03-04 22:05:25 +0000
2337@@ -102,6 +102,7 @@
2338 ('selection', _('Selection')),
2339 ('multiple', _('Multiple Selection')),
2340 ('open', _('Open')),
2341+ ('rational', _('Rational')),
2342 ]
2343 terms = [SimpleTerm(item, token=token, title=item)
2344 for token, item in items]
2345
2346=== added file 'src/schooltool/quiz/rational.py'
2347--- src/schooltool/quiz/rational.py 1970-01-01 00:00:00 +0000
2348+++ src/schooltool/quiz/rational.py 2013-03-04 22:05:25 +0000
2349@@ -0,0 +1,125 @@
2350+'''rational.py: Module to do rational arithmetic.
2351+
2352+ For full documentation, see http://www.nmt.edu/tcc/help/lang/python/examples/rational/.
2353+ Exports:
2354+ gcd ( a, b ):
2355+ [ a and b are integers ->
2356+ return the greatest common divisor of a and b ]
2357+ Rational ( a, b ):
2358+ [ (a is a nonnegative integer) and
2359+ (b is a positive integer) ->
2360+ return a new Rational instance with
2361+ numerator a and denominator b ]
2362+ .n: [ the numerator ]
2363+ .d: [ the denominator ]
2364+ .__add__(self, other):
2365+ [ other is a Rational instance ->
2366+ return the sum of self and other as a Rational instance ]
2367+ .__sub__(self, other):
2368+ [ other is a Rational instance ->
2369+ return the difference of self and other as a Rational
2370+ instance ]
2371+ .__mul__(self, other):
2372+ [ other is a Rational instance ->
2373+ return the product of self and other as a Rational
2374+ instance ]
2375+ .__div__(self, other):
2376+ [ other is a Rational instance ->
2377+ return the quotient of self and other as a Rational
2378+ instance ]
2379+ .__exp__(self, integer):
2380+ [ integer is an Integer instance ->
2381+ return self exponentiated to other as a Rational
2382+ instance ]
2383+ .__str__(self):
2384+ [ return a string representation of self ]
2385+ .__float__(self):
2386+ [ return a float approximation of self ]
2387+ .mixed(self):
2388+ [ return a string representation of self as a mixed
2389+ fraction ]
2390+'''
2391+def gcd ( a, b ):
2392+ '''Greatest common divisor function; Euclid's algorithm.
2393+
2394+ [ a and b are integers ->
2395+ return the greatest common divisor of a and b ]
2396+ '''
2397+ if b == 0:
2398+ return a
2399+ else:
2400+ return gcd(b, a%b)
2401+
2402+class Rational:
2403+ """An instance represents a rational number.
2404+ """
2405+ def __init__ ( self, a, b ):
2406+ """Constructor for Rational.
2407+ """
2408+ if b == 0:
2409+ raise ZeroDivisionError, ( "Denominator of a rational "
2410+ "may not be zero." )
2411+ else:
2412+ g = gcd ( a, b )
2413+ self.n = a / g
2414+ self.d = b / g
2415+ def __add__ ( self, other ):
2416+ """Add two rational numbers.
2417+ """
2418+ return Rational ( self.n * other.d + other.n * self.d,
2419+ self.d * other.d )
2420+ def __sub__ ( self, other ):
2421+ """Return self minus other.
2422+ """
2423+ return Rational ( self.n * other.d - other.n * self.d,
2424+ self.d * other.d )
2425+ def __mul__ ( self, other ):
2426+ """Implement multiplication.
2427+ """
2428+ return Rational ( self.n * other.n, self.d * other.d )
2429+ def __div__ ( self, other ):
2430+ """Implement division.
2431+ """
2432+ return Rational ( self.n * other.d, self.d * other.n )
2433+ def __pow__ ( self, integer):
2434+ """Implement division.
2435+ """
2436+ return Rational ( self.n ** integer, self.d ** integer )
2437+ def __str__ ( self ):
2438+ '''Display self as a string.
2439+ '''
2440+ return "%d/%d" % ( self.n, self.d )
2441+ def __repr__ (self):
2442+ """ Works
2443+ """
2444+ return str(self)
2445+ def __eq__ (self, other):
2446+ """ test for equality
2447+ """
2448+ if type(other) != type(self):
2449+ return False
2450+ return (self.d == other.d and self.n == other.n)
2451+ def __float__ ( self ):
2452+ """Implement the float() conversion function.
2453+ """
2454+ return float ( self.n ) / float ( self.d )
2455+ def mixed ( self ):
2456+ """Render self as a mixed fraction in string form.
2457+ """
2458+ #-- 1 --
2459+ # [ whole := self.n / self.d, truncated
2460+ # n2 := self.n % self.d ]
2461+ whole, n2 = divmod ( self.n, self.d )
2462+ #-- 2 --
2463+ # [ if self.d == 1 ->
2464+ # return str(self.n)
2465+ # else if whole == zero ->
2466+ # return str(n2)+"/"+str(self.d)
2467+ # else ->
2468+ # return str(whole)+" and "+str(n2)+"/"+str(self.d) ]
2469+ if self.d == 1:
2470+ return str(self.n)
2471+ elif whole == 0:
2472+ return "%s/%s" % (n2, self.d)
2473+ else:
2474+ return "%s and %s/%s" % (whole, n2, self.d)
2475
2476=== added file 'src/schooltool/quiz/utils.py'
2477--- src/schooltool/quiz/utils.py 1970-01-01 00:00:00 +0000
2478+++ src/schooltool/quiz/utils.py 2013-03-04 22:05:25 +0000
2479@@ -0,0 +1,135 @@
2480+#
2481+# SchoolTool - common information systems platform for school administration
2482+# Copyright (c) 2012 Shuttleworth Foundation,
2483+#
2484+# This program is free software; you can redistribute it and/or modify
2485+# it under the terms of the GNU General Public License as published by
2486+# the Free Software Foundation; either version 2 of the License, or
2487+# (at your option) any later version.
2488+#
2489+# This program is distributed in the hope that it will be useful,
2490+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2491+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2492+# GNU General Public License for more details.
2493+#
2494+# You should have received a copy of the GNU General Public License along
2495+# with this program; if not, write to the Free Software Foundation, Inc.,
2496+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
2497+#
2498+"""
2499+SchoolTool Quiz utility functions.
2500+"""
2501+
2502+from cgi import escape
2503+from docutils.core import publish_parts
2504+
2505+from schooltool.quiz import QuizMessage as _
2506+from schooltool.quiz.rst import QuizHTMLWriter
2507+
2508+import rational
2509+
2510+# XXX: customize and use IApplicationPreferences for this
2511+DATETIME_FORMAT = _('YYYY-MM-DD HH:MM:SS')
2512+DATETIME_INPUT_FORMAT = '%Y-%m-%d %H:%M:%S'
2513+DATE_FORMAT = _('YYYY-MM-DD')
2514+DATE_INPUT_FORMAT = '%Y-%m-%d'
2515+
2516+
2517+def render_rst(rst):
2518+ # XXX: sanitize before rendering
2519+ return publish_parts(rst, writer=QuizHTMLWriter())['body']
2520+
2521+
2522+def render_pre(text):
2523+ return '<pre>%s</pre>' % escape(text)
2524+
2525+
2526+def render_datetime(value):
2527+ return value.strftime(DATETIME_INPUT_FORMAT)
2528+
2529+
2530+def render_date(value):
2531+ return value.strftime(DATE_INPUT_FORMAT)
2532+
2533+
2534+def render_bool(value):
2535+ return [_('No'), _('Yes')][value]
2536+
2537+
2538+def format_percentage(number):
2539+ return '%.0f%%' % number
2540+
2541+
2542+def Validator(instring):
2543+ instring = str(instring)
2544+ inlist = instring.split(' ')
2545+ operators = ['+','-','*','/','**']
2546+ validated = []
2547+ while '' in inlist:
2548+ inlist.remove('')
2549+ for substring in inlist: #Convert to list
2550+ if substring in operators:
2551+ validated.append(substring)
2552+ elif CharacterValidation(substring):
2553+ #Convert to Rational
2554+ if "/" in substring:
2555+ parts = substring.split("/")
2556+ if len(parts) != 2:
2557+ return False
2558+ A = rational.Rational(int(parts[0]),int(parts[1]))
2559+ validated.append(A)
2560+ else:
2561+ A = rational.Rational(int(substring),1)
2562+ validated.append(A)
2563+ else:
2564+ return False
2565+ dup = 0
2566+ for item in validated: #Check for order
2567+ if dup == 0:
2568+ if type(item).__name__ != "instance":
2569+ return False
2570+ dup = 1
2571+ elif dup == 1:
2572+ if not item in operators:
2573+ return False
2574+ dup = 0
2575+ if validated[-1] in operators:
2576+ return False
2577+ for index in range(0,len(validated)-1):
2578+ if validated[index] == "**":
2579+ if validated[index+1].d != 1:
2580+ return False
2581+ return validated
2582+
2583+
2584+def CharacterValidation(string):
2585+ valid = "1234567890/"
2586+ for char in string:
2587+ if not char in valid:
2588+ return False
2589+ return True
2590+
2591+def Evaluator(validated):
2592+ if len(validated) == 1:
2593+ return validated[0]
2594+ for index in range(0,(len(validated)-1)):
2595+ if type(validated[index]) == type("") and validated[index] == "**":
2596+ result = validated[index-1] ** validated[index+1].n
2597+ return Evaluator(validated[:index-1]+[result]+validated[index+2:])
2598+ for index in range(0,(len(validated)-1)):
2599+ if type(validated[index]) == type("") and validated[index] == "*":
2600+ result = validated[index-1] * validated[index+1]
2601+ return Evaluator(validated[:index-1]+[result]+validated[index+2:])
2602+ elif type(validated[index]) == type("") and validated[index] == "/":
2603+ result = validated[index-1] / validated[index+1]
2604+ return Evaluator(validated[:index-1]+[result]+validated[index+2:])
2605+ for index in range(0,(len(validated)-1)):
2606+ if type(validated[index]) == type("") and validated[index] == "+":
2607+ result = validated[index-1] + validated[index+1]
2608+ return Evaluator(validated[:index-1]+[result]+validated[index+2:])
2609+ elif type(validated[index]) == type("") and validated[index] == "-":
2610+ result = validated[index-1] - validated[index+1]
2611+ return Evaluator(validated[:index-1]+[result]+validated[index+2:])
2612+
2613+def RationalValidator(studentstring, teacherstring):
2614+ return Evaluator(Validator(studentstring)) == Evaluator(Validator(teacherstring))
2615
2616=== removed file 'src/schooltool/quiz/utils.py'
2617--- src/schooltool/quiz/utils.py 2012-09-19 20:06:18 +0000
2618+++ src/schooltool/quiz/utils.py 1970-01-01 00:00:00 +0000
2619@@ -1,59 +0,0 @@
2620-#
2621-# SchoolTool - common information systems platform for school administration
2622-# Copyright (c) 2012 Shuttleworth Foundation,
2623-#
2624-# This program is free software; you can redistribute it and/or modify
2625-# it under the terms of the GNU General Public License as published by
2626-# the Free Software Foundation; either version 2 of the License, or
2627-# (at your option) any later version.
2628-#
2629-# This program is distributed in the hope that it will be useful,
2630-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2631-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2632-# GNU General Public License for more details.
2633-#
2634-# You should have received a copy of the GNU General Public License along
2635-# with this program; if not, write to the Free Software Foundation, Inc.,
2636-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
2637-#
2638-"""
2639-SchoolTool Quiz utility functions.
2640-"""
2641-
2642-from cgi import escape
2643-from docutils.core import publish_parts
2644-
2645-from schooltool.quiz import QuizMessage as _
2646-from schooltool.quiz.rst import QuizHTMLWriter
2647-
2648-
2649-# XXX: customize and use IApplicationPreferences for this
2650-DATETIME_FORMAT = _('YYYY-MM-DD HH:MM:SS')
2651-DATETIME_INPUT_FORMAT = '%Y-%m-%d %H:%M:%S'
2652-DATE_FORMAT = _('YYYY-MM-DD')
2653-DATE_INPUT_FORMAT = '%Y-%m-%d'
2654-
2655-
2656-def render_rst(rst):
2657- # XXX: sanitize before rendering
2658- return publish_parts(rst, writer=QuizHTMLWriter())['body']
2659-
2660-
2661-def render_pre(text):
2662- return '<pre>%s</pre>' % escape(text)
2663-
2664-
2665-def render_datetime(value):
2666- return value.strftime(DATETIME_INPUT_FORMAT)
2667-
2668-
2669-def render_date(value):
2670- return value.strftime(DATE_INPUT_FORMAT)
2671-
2672-
2673-def render_bool(value):
2674- return [_('No'), _('Yes')][value]
2675-
2676-
2677-def format_percentage(number):
2678- return '%.0f%%' % number

Subscribers

People subscribed via source and target branches

to all changes: