VC: Fix error on clone page for legacy-ID events
[cds-indico.git] / indico / MaKaC / paperReviewing.py
blob6cff27e51fa02a2d15ceb165a0dfe8823d13c198
1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
17 from MaKaC.common import log
18 from MaKaC.webinterface.mail import GenericNotification
19 from MaKaC.common.info import HelperMaKaCInfo
20 from MaKaC.webinterface import urlHandlers
21 from MaKaC.common.mail import GenericMailer
22 from MaKaC.common.timezoneUtils import getAdjustedDate, nowutc
23 from datetime import datetime
24 from persistent import Persistent
25 from MaKaC.errors import MaKaCError
26 import tempfile
27 from indico.core.config import Config
28 import conference
29 from MaKaC.common.Counter import Counter
30 from MaKaC.fossils.reviewing import IReviewingQuestionFossil, IReviewingStatusFossil
31 from MaKaC.common.fossilize import fossilizes, Fossilizable
32 from persistent.list import PersistentList
33 from MaKaC.i18n import _
35 ###############################################
36 # Conference-wide classes
37 ###############################################
40 class ConferencePaperReview(Persistent):
41 """
42 This class manages the parameters of the paper reviewing.
43 """
44 reviewingModes = ["", "No reviewing", "Content reviewing", "Layout reviewing", "Content and layout reviewing"]
45 predefinedStates = ["Accept", "To be corrected", "Reject"]
46 reviewingQuestionsAnswers = ["Strongly Disagree", "Disagree", "Weakly Disagree", "Borderline", "Weakly Agree", "Agree", "Strongly Agree"]
47 reviewingQuestionsLabels = ["-3", "", "", "0", "", "", "+3"]
48 initialSelectedAnswer = 3
49 # Reviewing constant values
50 NO_REVIEWING = 1
51 CONTENT_REVIEWING = 2
52 LAYOUT_REVIEWING = 3
53 CONTENT_AND_LAYOUT_REVIEWING = 4
55 def __init__( self, conference):
56 """ Constructor.
57 conference must be a Conference object (not an id).
58 """
60 self._conference = conference
62 #lists of users with reviewing roles
63 self._reviewersList = []
64 self._editorsList = []
65 self._refereesList = []
66 self._paperReviewManagersList = []
68 self._refereeContribution = {} #key: user, value: list of contributions where user is referee
69 self._editorContribution = {} #key: user, value: list of contributions where user is editor
70 self._reviewerContribution = {} #key: user, value: list of contributions where user is reviewer
72 self.setChoice(self.NO_REVIEWING) #initial reviewing mode: no reviewing
74 #default dates
75 self._startSubmissionDate = None
76 self._endSubmissionDate = None
77 self._defaultRefereeDueDate = None
78 self._defaultEditorDueDate = None
79 self._defaultReviwerDueDate = None
81 #auto e-mails
82 self._enablePRMEmailNotif = True
83 self._enableRefereeEmailNotif = False
84 self._enableEditorEmailNotif = False
85 self._enableReviewerEmailNotif = False
86 self._enableRefereeEmailNotifForContribution = False
87 self._enableEditorEmailNotifForContribution = False
88 self._enableReviewerEmailNotifForContribution = False
89 self._enableRefereeJudgementEmailNotif = False
90 self._enableEditorJudgementEmailNotif = False
91 self._enableReviewerJudgementEmailNotif = False
92 self._enableAuthorSubmittedMatRefereeEmailNotif = False
93 self._enableAuthorSubmittedMatEditorEmailNotif = False
94 self._enableAuthorSubmittedMatReviewerEmailNotif = False
95 self._enableEditorSubmittedRefereeEmailNotif = False
96 self._enableReviewerSubmittedRefereeEmailNotif = False
98 self._statuses = [] # list of content reviewing and final judgement
99 self._reviewingQuestions = [] #list of content reviewing and final judgement questions
100 self._layoutQuestions = [] #list of layout editing criteria
101 self._templates = {} #dictionary with layout templates. key: id, value: Template object
102 self._templateCounter = Counter(1) #counter to make new id's for the templates
103 self._userCompetences = {} #dictionary with the competences of each user. key: user, value: list of competences
104 self._userCompetencesByTag = {} #dictionary with the users for each competence. key: competence, value: list of users
105 self._reviewingMaterials = {}
106 self.notifyModification()
108 # id generators
109 self._statusCounter = Counter(1)
110 self._questionCounter = Counter(1)
111 self._answerCounter = Counter(1)
112 self.addStatus("Accept", False)
113 self.addStatus("To be corrected", False)
114 self.addStatus("Reject", False)
117 def getConference(self):
118 """ Returns the parent conference of the ConferencePaperReview object
120 return self._conference
122 def setStartSubmissionDate(self, startSubmissionDate):
123 self._startSubmissionDate= datetime(startSubmissionDate.year,startSubmissionDate.month,startSubmissionDate.day,23,59,59)
125 def getStartSubmissionDate(self):
126 return self._startSubmissionDate
128 def setEndSubmissionDate(self, endSubmissionDate):
129 self._endSubmissionDate = datetime(endSubmissionDate.year,endSubmissionDate.month,endSubmissionDate.day,23,59,59)
131 def getEndSubmissionDate(self):
132 return self._endSubmissionDate
134 def setDefaultRefereeDueDate(self, date):
135 self._defaultRefereeDueDate = date
137 def getDefaultRefereeDueDate(self):
138 return self._defaultRefereeDueDate
140 def getAdjustedDefaultRefereeDueDate(self):
141 if self.getDefaultRefereeDueDate() is None:
142 return None
143 else:
144 return getAdjustedDate(self._defaultRefereeDueDate, self.getConference())
146 def setDefaultEditorDueDate(self, date):
147 self._defaultEditorDueDate = date
149 def getDefaultEditorDueDate(self):
150 return self._defaultEditorDueDate
152 def getAdjustedDefaultEditorDueDate(self):
153 if self.getDefaultEditorDueDate() is None:
154 return None
155 else:
156 return getAdjustedDate(self._defaultEditorDueDate, self.getConference())
158 def setDefaultReviewerDueDate(self, date):
159 self._defaultReviwerDueDate = date
161 def getDefaultReviewerDueDate(self):
162 return self._defaultReviwerDueDate
164 def getAdjustedDefaultReviewerDueDate(self):
165 if self.getDefaultReviewerDueDate() is None:
166 return None
167 else:
168 return getAdjustedDate(self._defaultReviwerDueDate, self.getConference())
170 #auto e-mails methods for assign/remove reviewing team to the conference notification
171 def getEnablePRMEmailNotif(self):
172 return self._enablePRMEmailNotif
174 def enablePRMEmailNotif(self):
175 self._enablePRMEmailNotif = True
177 def disablePRMEmailNotif(self):
178 self._enablePRMEmailNotif = False
180 def getEnableRefereeEmailNotif(self):
181 return self._enableRefereeEmailNotif
183 def enableRefereeEmailNotif(self):
184 self._enableRefereeEmailNotif = True
186 def disableRefereeEmailNotif(self):
187 self._enableRefereeEmailNotif = False
190 def getEnableEditorEmailNotif(self):
191 return self._enableEditorEmailNotif
193 def enableEditorEmailNotif(self):
194 self._enableEditorEmailNotif = True
196 def disableEditorEmailNotif(self):
197 self._enableEditorEmailNotif = False
200 def getEnableReviewerEmailNotif(self):
201 return self._enableReviewerEmailNotif
203 def enableReviewerEmailNotif(self):
204 self._enableReviewerEmailNotif = True
206 def disableReviewerEmailNotif(self):
207 self._enableReviewerEmailNotif = False
210 #auto e-mails methods for assign/remove reviewers to/from contributions notification
211 def getEnableRefereeEmailNotifForContribution(self):
212 return self._enableRefereeEmailNotifForContribution
214 def enableRefereeEmailNotifForContribution(self):
215 self._enableRefereeEmailNotifForContribution = True
217 def disableRefereeEmailNotifForContribution(self):
218 self._enableRefereeEmailNotifForContribution = False
220 def getEnableEditorEmailNotifForContribution(self):
221 return self._enableEditorEmailNotifForContribution
223 def enableEditorEmailNotifForContribution(self):
224 self._enableEditorEmailNotifForContribution = True
226 def disableEditorEmailNotifForContribution(self):
227 self._enableEditorEmailNotifForContribution = False
229 def getEnableReviewerEmailNotifForContribution(self):
230 return self._enableReviewerEmailNotifForContribution
232 def enableReviewerEmailNotifForContribution(self):
233 self._enableReviewerEmailNotifForContribution = True
235 def disableReviewerEmailNotifForContribution(self):
236 self._enableReviewerEmailNotifForContribution = False
238 #auto e-mails methods for reviewers judgements notification
239 def getEnableRefereeJudgementEmailNotif(self):
240 return self._enableRefereeJudgementEmailNotif
242 def enableRefereeJudgementEmailNotif(self):
243 self._enableRefereeJudgementEmailNotif = True
245 def disableRefereeJudgementEmailNotif(self):
246 self._enableRefereeJudgementEmailNotif = False
248 def getEnableEditorJudgementEmailNotif(self):
249 return self._enableEditorJudgementEmailNotif
251 def enableEditorJudgementEmailNotif(self):
252 self._enableEditorJudgementEmailNotif = True
254 def disableEditorJudgementEmailNotif(self):
255 self._enableEditorJudgementEmailNotif = False
257 def getEnableReviewerJudgementEmailNotif(self):
258 return self._enableReviewerJudgementEmailNotif
260 def enableReviewerJudgementEmailNotif(self):
261 self._enableReviewerJudgementEmailNotif = True
263 def disableReviewerJudgementEmailNotif(self):
264 self._enableReviewerJudgementEmailNotif = False
266 def getEnableEditorSubmittedRefereeEmailNotif(self):
267 if not hasattr(self, "_enableEditorSubmittedRefereeEmailNotif"):
268 self._enableEditorSubmittedRefereeEmailNotif = False
269 return self._enableEditorSubmittedRefereeEmailNotif
271 def enableEditorSubmittedRefereeEmailNotif(self):
272 self._enableEditorSubmittedRefereeEmailNotif = True
274 def disableEditorSubmittedRefereeEmailNotif(self):
275 self._enableEditorSubmittedRefereeEmailNotif = False
277 def getEnableReviewerSubmittedRefereeEmailNotif(self):
278 if not hasattr(self, "_enableReviewerSubmittedRefereeEmailNotif"):
279 self._enableReviewerSubmittedRefereeEmailNotif = False
280 return self._enableReviewerSubmittedRefereeEmailNotif
282 def enableReviewerSubmittedRefereeEmailNotif(self):
283 self._enableReviewerSubmittedRefereeEmailNotif = True
285 def disableReviewerSubmittedRefereeEmailNotif(self):
286 self._enableReviewerSubmittedRefereeEmailNotif = False
288 #auto e-mails methods for authors' submittion materials notification
289 def getEnableAuthorSubmittedMatRefereeEmailNotif(self):
290 return self._enableAuthorSubmittedMatRefereeEmailNotif
292 def enableAuthorSubmittedMatRefereeEmailNotif(self):
293 self._enableAuthorSubmittedMatRefereeEmailNotif = True
295 def disableAuthorSubmittedMatRefereeEmailNotif(self):
296 self._enableAuthorSubmittedMatRefereeEmailNotif = False
298 def getEnableAuthorSubmittedMatEditorEmailNotif(self):
299 return self._enableAuthorSubmittedMatEditorEmailNotif
301 def enableAuthorSubmittedMatEditorEmailNotif(self):
302 self._enableAuthorSubmittedMatEditorEmailNotif = True
304 def disableAuthorSubmittedMatEditorEmailNotif(self):
305 self._enableAuthorSubmittedMatEditorEmailNotif = False
307 def getEnableAuthorSubmittedMatReviewerEmailNotif(self):
308 return self._enableAuthorSubmittedMatReviewerEmailNotif
310 def enableAuthorSubmittedMatReviewerEmailNotif(self):
311 self._enableAuthorSubmittedMatReviewerEmailNotif = True
313 def disableAuthorSubmittedMatReviewerEmailNotif(self):
314 self._enableAuthorSubmittedMatReviewerEmailNotif = False
316 #Reviewing mode methods
317 def setChoice(self, choice):
318 """ Sets the reviewing mode for the conference, as a number and as a string
319 1: "No reviewing"
320 2: "Content reviewing"
321 3: "Layout reviewing"
322 4: "Content and layout reviewing"
324 self._choice = choice
325 self._reviewing = ConferencePaperReview.reviewingModes[choice]
327 def getChoice(self):
328 """ Returns the reviewing mode for the conference, as a number
329 1: "No reviewing"
330 2: "Content reviewing"
331 3: "Layout reviewing"
332 4: "Content and layout reviewing"
334 return self._choice
336 def getReviewingMode(self):
337 """ Returns the reviewing mode for the conference, as a string
339 return self._reviewing
341 def setReviewingMode(self, mode):
342 """ Sets the reviewing mode of the conference, as a string.
343 The string has to be one of those in ConferencePaperReview.reviewingModes
345 if mode in ConferencePaperReview.reviewingModes:
346 self._reviewing = mode
347 self._choice = ConferencePaperReview.reviewingModes.index(mode)
349 def hasReviewing(self):
350 """ Convenience method that returns if the Conference has a reviewing mode active or not.
352 return self._choice > 1
354 def hasPaperReviewing(self):
355 """ Convenience method that returns if the Conference has a reviewing mode that allows paper reviewing.
356 (modes 2 and 4)
358 return self._choice == self.CONTENT_REVIEWING or self._choice == self.CONTENT_AND_LAYOUT_REVIEWING
360 def hasPaperEditing(self):
361 """ Convenience method that returns if the Conference has a reviewing mode that allows paper editing.
362 (modes 3 and 4)
364 return self._choice == self.LAYOUT_REVIEWING or self._choice == self.CONTENT_AND_LAYOUT_REVIEWING
366 def inModificationPeriod(self):
367 date = nowutc()
368 if date <= self._endCorrectionDate:
369 return True
370 else:
371 return False
373 def inSubmissionPeriod(self):
374 date = nowutc()
375 if date <= self._endSubmissionDate & date >= self._startSubmissionDate:
376 return True
377 else:
378 return False
380 def inEditingPeriod(self):
381 date = nowutc()
382 if date <= self._endEditingDate:
383 return True
384 else:
385 return False
387 def inReviewingPeriod(self):
388 date = nowutc()
389 if date <= self._endReviewingDate & date >= self._endCorrectionDate:
390 return True
391 else:
392 return False
394 # Configurable options (for the future)
395 def getNumberOfAnswers(self):
396 """ Returns the number of possible answers (radio buttons) per question
398 return len(ConferencePaperReview.reviewingQuestionsLabels)
401 # status methods
402 def addStatus(self, name, editable):
403 """ Adds this status at the end of the list of statuses list
405 newId = self.getNewStatusId()
406 status = Status(newId,name,editable)
407 self._statuses.append(status)
408 self.notifyModification()
410 def getStatusById(self, statusId):
411 """ Return the status with the especified id """
412 for status in self._statuses:
413 if (statusId == status.getId()):
414 return status
416 def getStatuses(self):
417 """ Returns the list of statuses
419 # Filter the non editable statuses
420 editableStatuses = []
421 for status in self._statuses:
422 if status.getEditable():
423 editableStatuses.append(status)
424 return editableStatuses
426 def getDefaultStatusesDictionary(self):
427 """ Returns a dictionary with the default statuses {id:name, ...}
429 dic = {}
430 for status in self._statuses:
431 # 1: Accept, 2: To be corrected, 3: Reject
432 if ((status.getId() == "1") or (status.getId() == "2") or (status.getId() == "3")):
433 dic[status.getId()] = status.getName()
434 return dic
437 def getStatusesDictionary(self):
438 """ Returns a dictionary with the names of the statuses and the ids
440 dic = {}
441 for status in self._statuses:
442 dic[status.getId()] = status.getName()
443 return dic
445 def removeStatus(self, statusId):
446 """ Removes a status from the list
448 status = self.getStatusById(statusId)
450 if status:
451 self._statuses.remove(status)
452 self.notifyModification()
453 else:
454 raise MaKaCError("Cannot remove a status which doesn't exist")
456 def editStatus(self, statusId, newName):
457 """ Edit the name of a status
459 status = self.getStatusById(statusId)
461 if status:
462 status.setName(newName)
463 self.notifyModification()
464 else:
465 raise MaKaCError("Cannot edit a question which doesn't exist")
468 def getNewStatusId(self):
469 """ Returns a new an unused questionId
470 Increments the questionId counter
472 return self._statusCounter.newCount()
475 # content reviewing and final judgement questions methods
476 def addReviewingQuestion(self, text):
477 """ Adds this question at the end of the list of questions
479 newId = self.getNewQuestionId()
480 question = Question(newId,text)
481 self._reviewingQuestions.append(question)
482 self.notifyModification()
484 def getReviewingQuestionById(self, questionId):
485 """ Return the question with the especified id """
486 for question in self._reviewingQuestions:
487 if (questionId == question.getId()):
488 return question
490 def getReviewingQuestions(self):
491 """ Returns the list of questions
493 return self._reviewingQuestions
495 def removeReviewingQuestion(self, questionId):
496 """ Removes a question from the list
498 question = self.getReviewingQuestionById(questionId)
500 if question:
501 self._reviewingQuestions.remove(question)
502 self.notifyModification()
503 else:
504 raise MaKaCError("Cannot remove a question which doesn't exist")
506 def editReviewingQuestion(self, questionId, text):
507 """ Edit the text of a question
509 question = self.getReviewingQuestionById(questionId)
511 if question:
512 question.setText(text)
513 self.notifyModification()
514 else:
515 raise MaKaCError("Cannot edit a question which doesn't exist")
518 # layout editing criteria methods
519 def addLayoutQuestion(self, text):
520 """ Add a new layout editing criterion
522 newId = self.getNewQuestionId()
523 question = Question(newId,text)
524 self._layoutQuestions.append(question)
525 self.notifyModification()
527 def getLayoutQuestionById(self, questionId):
528 """ Return the question with the especified id """
529 for question in self._layoutQuestions:
530 if (questionId == question.getId()):
531 return question
533 def getLayoutQuestions(self):
534 """ Get the list of all the layout criteria
536 return self._layoutQuestions
538 def removeLayoutQuestion(self, questionId):
539 """ Remove one the layout question
541 question = self.getLayoutQuestionById(questionId)
543 if question:
544 self._layoutQuestions.remove(question)
545 self.notifyModification()
546 else:
547 raise MaKaCError("Cannot remove a question which doesn't exist")
549 def editLayoutQuestion(self, questionId, text):
550 """ Edit the text of a question
552 question = self.getLayoutQuestionById(questionId)
554 if question:
555 question.setText(text)
556 self.notifyModification()
557 else:
558 raise MaKaCError("Cannot edit a question which doesn't exist")
560 def getNewQuestionId(self):
561 """ Returns a new an unused questionId
562 Increments the questionId counter
564 return self._questionCounter.newCount()
566 #referee methods
567 def addReferee(self, newReferee):
568 """ Adds a new referee to the conference.
569 newReferee has to be an Avatar object.
570 The referee is sent a mail notification only if the manager has enabled the option in 'Automatic e-mails' section.
572 if newReferee in self.getRefereesList():
573 raise MaKaCError("This referee has already been added to this conference")
574 else:
575 self._refereesList.append(newReferee)
576 newReferee.linkTo(self._conference, "referee")
577 if not self._userCompetences.has_key(newReferee):
578 self._userCompetences[newReferee] = []
579 self.notifyModification()
580 if self._enableRefereeEmailNotif == True:
581 notification = ConferenceReviewingNotification(newReferee, 'Referee', self._conference)
582 GenericMailer.sendAndLog(notification, self._conference,
583 log.ModuleNames.PAPER_REVIEWING)
586 def removeReferee(self, referee):
587 """ Remove a referee from the conference.
588 referee has to be an Avatar object.
589 The referee is sent a mail notification only if the manager has enabled the option in 'Automatic e-mails' section.
591 if referee in self._refereesList:
592 if self._userCompetences.has_key(referee):
593 if not ((referee in self._editorsList) or \
594 (referee in self._paperReviewManagersList) or \
595 (referee in self._reviewersList)):
596 self.clearUserCompetences(referee)
597 del(self._userCompetences[referee])
598 self._refereesList.remove(referee)
599 referee.unlinkTo(self._conference, "referee")
600 self.notifyModification()
601 if self._enableRefereeEmailNotif == True:
602 notification = ConferenceReviewingRemoveNotification(referee, 'Referee', self._conference)
603 GenericMailer.sendAndLog(notification, self._conference,
604 log.ModuleNames.PAPER_REVIEWING)
605 else:
606 raise MaKaCError("Cannot remove a referee who is not yet referee")
608 def isReferee(self, user):
609 """ Returns if a given user is referee of the conference.
610 user has to be an Avatar object.
612 return user in self._refereesList
614 def getRefereesList(self):
615 """ Returns the list of referees as a list of Avatar objects.
617 return self._refereesList
619 def addRefereeContribution(self, referee, contribution):
620 """ Adds the contribution to the list of contributions for a given referee.
621 referee has to be an Avatar object.
623 if self._refereeContribution.has_key(referee):
624 self._refereeContribution[referee].append(contribution)
625 self._refereeContribution[referee].sort(key= lambda c: int(c.getId()))
626 else:
627 self._refereeContribution[referee] = [contribution]
628 self.notifyModification()
630 def removeRefereeContribution(self, referee, contribution):
631 """ Removes the contribution from the list of contributions of this referee
632 referee has to be an Avatar object.
634 if self._refereeContribution.has_key(referee):
635 self._refereeContribution[referee].remove(contribution)
636 self.notifyModification()
638 def isRefereeContribution(self, referee, contribution):
639 """ Returns if a user is referee for a given contribution
640 referee has to be an Avatar object.
642 return self._refereeContribution.has_key(referee) and contribution in self._refereeContribution[referee]
644 def getJudgedContributions(self, referee):
645 """ Returns the list of contributions for a given referee
646 referee has to be an Avatar object.
648 if self._refereeContribution.has_key(referee):
649 return self._refereeContribution[referee]
650 else:
651 return []
653 #editor methods
654 def addEditor(self, newEditor):
655 """ Adds a new editor to the conference.
656 editor has to be an Avatar object.
657 The editor is sent a mail notification only if the manager has enabled the option in 'Automatic e-mails' section.
659 if newEditor in self.getEditorsList():
660 raise MaKaCError("This editor has already been added to this conference")
661 else:
662 self._editorsList.append(newEditor)
663 newEditor.linkTo(self._conference, "editor")
664 if not self._userCompetences.has_key(newEditor):
665 self._userCompetences[newEditor] = []
666 self.notifyModification()
667 if self._enableEditorEmailNotif == True:
668 notification = ConferenceReviewingNotification(newEditor, 'Layout Reviewer', self._conference)
669 GenericMailer.sendAndLog(notification, self._conference,
670 log.ModuleNames.PAPER_REVIEWING)
672 def removeEditor(self, editor):
673 """ Remove a editor from the conference.
674 editor has to be an Avatar object.
675 The editor is sent a mail notification only if the manager has enabled the option in 'Automatic e-mails' section.
677 if editor in self._editorsList:
678 if self._userCompetences.has_key(editor):
679 if not ((editor in self._paperReviewManagersList) or \
680 (editor in self._reviewersList) or \
681 (editor in self._refereesList)):
682 self.clearUserCompetences(editor)
683 del(self._userCompetences[editor])
684 self._editorsList.remove(editor)
685 editor.unlinkTo(self._conference, "editor")
686 self.notifyModification()
687 if self._enableEditorEmailNotif == True:
688 notification = ConferenceReviewingRemoveNotification(editor, 'Layout Reviewer', self._conference)
689 GenericMailer.sendAndLog(notification, self._conference,
690 log.ModuleNames.PAPER_REVIEWING)
691 else:
692 raise MaKaCError("Cannot remove an editor who is not yet editor")
694 def isEditor(self, user):
695 """ Returns if a given user is editor of the conference.
696 user has to be an Avatar object.
698 return user in self._editorsList
700 def getEditorsList(self):
701 """ Returns the list of editors as a list of users.
703 return self._editorsList
705 def addEditorContribution(self, editor, contribution):
706 """ Adds the contribution to the list of contributions for a given editor.
707 editor has to be an Avatar object.
709 if self._editorContribution.has_key(editor):
710 self._editorContribution[editor].append(contribution)
711 self._editorContribution[editor].sort(key= lambda c: int(c.getId()))
712 else:
713 self._editorContribution[editor] = [contribution]
714 self.notifyModification()
716 def removeEditorContribution(self, editor, contribution):
717 """ Removes the contribution from the list of contributions of this editor
718 editor has to be an Avatar object.
720 if self._editorContribution.has_key(editor):
721 self._editorContribution[editor].remove(contribution)
722 self.notifyModification()
724 def isEditorContribution(self, editor, contribution):
725 """ Returns if a user is editor for a given contribution
726 editor has to be an Avatar object.
728 return self._editorContribution.has_key(editor) and contribution in self._editorContribution[editor]
730 def getEditedContributions(self, editor):
731 """ Returns the list of contributions for a given editor
732 editor has to be an Avatar object.
734 if self._editorContribution.has_key(editor):
735 return self._editorContribution[editor]
736 else:
737 return []
739 #reviewer methods
740 def addReviewer(self, newReviewer):
741 """ Adds a new reviewer to the conference.
742 newreviewer has to be an Avatar object.
743 The reviewer is sent a mail notification only if the manager has enabled the option in 'Automatic e-mails' section.
745 if newReviewer in self.getReviewersList():
746 raise MaKaCError("This reviewer has already been added to this conference")
747 else:
748 self._reviewersList.append(newReviewer)
749 newReviewer.linkTo(self._conference, "reviewer")
750 if not self._userCompetences.has_key(newReviewer):
751 self._userCompetences[newReviewer] = []
752 self.notifyModification()
753 if self._enableReviewerEmailNotif == True:
754 notification = ConferenceReviewingNotification(newReviewer, 'Content Reviewer', self._conference)
755 GenericMailer.sendAndLog(notification, self._conference,
756 log.ModuleNames.PAPER_REVIEWING)
758 def removeReviewer(self, reviewer):
759 """ Remove a reviewer from the conference.
760 reviewer has to be an Avatar object.
761 The reviewer is sent a mail notification only if the manager has enabled the option in 'Automatic e-mails' section.
763 if reviewer in self._reviewersList:
764 if self._userCompetences.has_key(reviewer):
765 if not ((reviewer in self._editorsList) or \
766 (reviewer in self._paperReviewManagersList) or \
767 (reviewer in self._refereesList)):
768 self.clearUserCompetences(reviewer)
769 del(self._userCompetences[reviewer])
770 self._reviewersList.remove(reviewer)
771 reviewer.unlinkTo(self._conference, "reviewer")
772 self.notifyModification()
773 if self._enableReviewerEmailNotif == True:
774 notification = ConferenceReviewingRemoveNotification(reviewer, 'Content Reviewer', self._conference)
775 GenericMailer.sendAndLog(notification, self._conference,
776 log.ModuleNames.PAPER_REVIEWING)
777 else:
778 raise MaKaCError("Cannot remove a reviewer who is not yet reviewer")
780 def isReviewer(self, user):
781 """ Returns if a given user is reviewer of the conference.
782 user has to be an Avatar object.
784 return user in self._reviewersList
786 def getReviewersList(self):
787 """ Returns the list of reviewers as a list of users.
789 return self._reviewersList
791 def addReviewerContribution(self, reviewer, contribution):
792 """ Adds the contribution to the list of contributions for a given reviewer.
793 reviewer has to be an Avatar object.
795 if self._reviewerContribution.has_key(reviewer):
796 self._reviewerContribution[reviewer].append(contribution)
797 self._reviewerContribution[reviewer].sort(key= lambda c: int(c.getId()))
798 else:
799 self._reviewerContribution[reviewer] = [contribution]
800 self.notifyModification()
802 def removeReviewerContribution(self, reviewer, contribution):
803 """ Removes the contribution from the list of contributions of this reviewer
804 reviewer has to be an Avatar object.
806 if self._reviewerContribution.has_key(reviewer):
807 self._reviewerContribution[reviewer].remove(contribution)
808 self.notifyModification()
810 def isReviewerContribution(self, reviewer, contribution):
811 """ Returns if a user is reviewer for a given contribution
812 reviewer has to be an Avatar object.
814 return self._reviewerContribution.has_key(reviewer) and contribution in self._reviewerContribution[reviewer]
816 def getReviewedContributions(self, reviewer):
817 """ Returns the list of contributions for a given reviewer
818 reviewer has to be an Avatar object.
820 if self._reviewerContribution.has_key(reviewer):
821 return self._reviewerContribution[reviewer]
822 else:
823 return []
825 #paper review manager methods
826 def addPaperReviewManager(self, newPaperReviewManager):
827 """ Adds a new paper review manager to the conference.
828 newPaperReviewManager has to be an Avatar object.
829 The paper review manager is sent a mail notification.
831 if newPaperReviewManager in self.getPaperReviewManagersList():
832 raise MaKaCError("This paper review manager has already been added to this conference")
833 else:
834 self._paperReviewManagersList.append(newPaperReviewManager)
835 newPaperReviewManager.linkTo(self._conference, "paperReviewManager")
836 if not self._userCompetences.has_key(newPaperReviewManager):
837 self._userCompetences[newPaperReviewManager] = []
838 self.notifyModification()
839 if self._enablePRMEmailNotif == True:
840 notification = ConferenceReviewingNotification(newPaperReviewManager, 'Paper Review Manager', self._conference)
841 GenericMailer.sendAndLog(notification, self._conference,
842 log.ModuleNames.PAPER_REVIEWING)
844 def removePaperReviewManager(self, paperReviewManager):
845 """ Remove a paper review manager from the conference.
846 paperReviewManager has to be an Avatar object.
847 The paper review manager is sent a mail notification.
849 if paperReviewManager in self._paperReviewManagersList:
850 if self._userCompetences.has_key(paperReviewManager):
851 if not ((paperReviewManager in self._editorsList) or \
852 (paperReviewManager in self._reviewersList) or \
853 (paperReviewManager in self._refereesList)):
854 self.clearUserCompetences(paperReviewManager)
855 del(self._userCompetences[paperReviewManager])
856 self._paperReviewManagersList.remove(paperReviewManager)
857 paperReviewManager.unlinkTo(self._conference, "paperReviewManager")
858 self.notifyModification()
859 if self._enablePRMEmailNotif == True:
860 notification = ConferenceReviewingRemoveNotification(paperReviewManager, 'Paper Review Manager', self._conference)
861 GenericMailer.sendAndLog(notification, self._conference,
862 log.ModuleNames.PAPER_REVIEWING)
863 else:
864 raise MaKaCError("Cannot remove a paper review manager who is not yet paper review manager")
866 def isPaperReviewManager(self, user):
867 """ Returns if a given user is paper review manager of the conference.
868 user has to be an Avatar object.
870 return user in self._paperReviewManagersList
872 def getPaperReviewManagersList(self):
873 """ Returns the list of paper review managers as a list of users.
875 return self._paperReviewManagersList
877 # templates methods
878 def setTemplate(self, name, description, format, fd, id, extension = ".template"):
879 """ Stores a template.
880 There can be 1 template for any format.
883 cfg = Config.getInstance()
884 tempPath = cfg.getUploadedFilesTempDir()
885 tempFileName = tempfile.mkstemp( suffix="MaKaC.tmp", dir = tempPath )[1]
886 f = open( tempFileName, "wb" )
887 f.write( fd.read() )
888 f.close()
890 #id = self.getNewTemplateId()
892 fileName = "Contribution_template_" + id + "_c" + self._conference.id + extension
894 file = conference.LocalFile()
895 file.setName( fileName )
896 file.setDescription( "Paper reviewing template with id " + id + "with format: " + format + " of the conference " + self._conference.id )
897 file.setFileName( fileName )
899 file.setFilePath( tempFileName )
900 file.setOwner( self._conference )
901 file.setId( fileName )
902 file.archive( self._conference._getRepository() )
904 self._templates[id] = Template(id, self._conference, name, description, format, file)
905 self.notifyModification()
907 def hasTemplates(self):
908 """ Returns if the conference has any reviewing templates
910 return len(self._templates) > 0
912 def getTemplates(self):
913 """ Returns a dictionary of templates. key: id, value: Template object
915 return self._templates
917 def getNewTemplateId(self):
918 """ Returns a new an unused templateId
919 Increments the templateId counter
921 return self._templateCounter.newCount()
923 def deleteTemplate(self, id):
924 """ Removes a reviewing template from the conference, given the id.
926 del self._templates[id]
927 self.notifyModification()
930 #competences methods
931 def isInReviewingTeam(self, user):
932 return user in self._paperReviewManagersList or \
933 user in self._refereesList or \
934 user in self._editorsList or \
935 user in self._reviewersList or \
936 user in self._reviewersList
938 def setUserCompetences(self, user, competences):
939 """ Sets a list of competences (a list of strings) to a given user
941 if self.isInReviewingTeam(user):
942 self.clearUserCompetences(user)
943 self.addUserCompetences(user, competences)
944 else:
945 raise MaKaCError("""User %s is not in the reviewing team for this conference, so you cannot set competences for him/her"""%user.getFullName())
947 def clearUserCompetences(self, user):
948 if self.isInReviewingTeam(user):
949 for c in self.getCompetencesByUser(user):
950 self._userCompetencesByTag[c].remove(user)
951 if len(self._userCompetencesByTag[c]) == 0:
952 del self._userCompetencesByTag[c]
953 self._userCompetences[user] = []
954 else:
955 raise MaKaCError("""User %s is not in the reviewing team for this conference, so you cannot clear competences for him/her"""%user.getFullName())
957 def addUserCompetences(self, user, competences):
958 """ Adds a list of competences (a list of strings) to a given user
960 if self.isInReviewingTeam(user):
961 self._userCompetences[user].extend(competences)
962 for c in competences:
963 if self._userCompetencesByTag.has_key(c):
964 self._userCompetencesByTag[c].append(user)
965 else:
966 self._userCompetencesByTag[c] = [user]
967 self.notifyModification()
968 else:
969 raise MaKaCError("""User %s is not in the reviewing team for this conference, so you cannot add competences for him/her"""%user.getFullName())
971 def removeUserCompetences(self, user, competences):
972 """ Removes a list of competences (a list of strings) from a given user
974 if self.isInReviewingTeam(user):
975 if self._userCompetences.has_key(user):
976 for c in competences:
977 self._userCompetences[user].remove(c)
978 self._userCompetencesByTag[c].remove(user)
979 if len(self._userCompetencesByTag[c]) == 0:
980 del self._userCompetencesByTag[c]
981 self.notifyModification()
982 else:
983 raise MaKaCError("""User %s does not have competences"""%(user.getFullName))
984 else:
985 raise MaKaCError("""User %s is not in the reviewing team for this conference, so you cannot remove competences from him/her"""%user.getFullName())
987 def getAllUserCompetences(self, onlyActive = False, role = 'all'):
988 """ Returns a list with the users and their competences.
989 role can be 'referee', 'editor', 'reviewer' or 'all' (a different value or none specified defaults to 'all')
990 The list is composed of tuples of 2 elements: (user, competenceList) where competenceList is a list of strings.
991 The list is ordered by the full name of the users.
994 if onlyActive:
995 if role == 'referee':
996 users = self._refereesList
997 elif role == 'editor':
998 users = self._editorsList
999 elif role == 'reviewer':
1000 users = self._reviewersList
1001 else:
1002 users = self._paperReviewManagersList + self._refereesList + self._editorsList + self._reviewersList
1003 else:
1004 if role == 'referee':
1005 users = [u for u in self._userCompetences.keys() if u in self._refereesList]
1006 elif role == 'editor':
1007 users = [u for u in self._userCompetences.keys() if u in self._editorsList]
1008 elif role == 'reviewer':
1009 users = [u for u in self._userCompetences.keys() if u in self._reviewersList]
1010 else:
1011 users = self._userCompetences.keys()
1013 users.sort(key = lambda u: u.getFullName())
1014 return zip(users, [self._userCompetences[user] for user in users])
1016 def getCompetencesByUser(self, user):
1017 """ Returns the list of competences (a list of strings) for a given user.
1019 return self._userCompetences[user]
1021 def getUserReviewingRoles(self, user):
1022 """ Returns the list of roles (PRM, referee, editor, reviewer) that a given user has
1023 in this conference, as a list of strings.
1025 roles=[]
1026 if self.isPaperReviewManager(user) and not self._choice == self.NO_REVIEWING:
1027 roles.append('Manager of Paper Reviewing Module')
1028 if self.isReferee(user) and (self._choice == self.CONTENT_REVIEWING or self._choice == self.CONTENT_AND_LAYOUT_REVIEWING):
1029 roles.append('Referee')
1030 if self.isEditor(user) and (self._choice == self.LAYOUT_REVIEWING or self._choice == self.CONTENT_AND_LAYOUT_REVIEWING):
1031 roles.append('Layout Reviewer')
1032 if self.isReviewer(user) and (self._choice == self.CONTENT_REVIEWING or self._choice == self.CONTENT_AND_LAYOUT_REVIEWING):
1033 roles.append('Content Reviewer')
1034 return roles
1036 def notifyModification(self):
1037 """ Notifies the DB that a list or dictionary attribute of this object has changed
1039 self._p_changed = 1
1043 class Template(Persistent):
1044 """ This class represents a template for contribution reviewing.
1045 A template is a file uploaded by a Conference Manager or a Paper Review Manager.
1046 Normal users can download it to have an idea of the format their contribution should have.
1047 A conference can have many of these templates.
1050 formats = ["Word",
1051 "OpenOffice Writer",
1052 "PowerPoint",
1053 "OpenOffice Impress",
1054 "LaTeX"]
1056 def __init__(self, id, conference, name, description, format, file):
1057 self.__id = id
1058 self.__conf = conference
1059 self.__name = name
1060 self.__description = description
1061 self.__format = format
1062 self.__file = file
1064 def getId(self):
1065 return self.__id
1067 def getName(self):
1068 return self.__name
1070 def getDescription(self):
1071 return self.__description
1073 def getFormat(self):
1074 return self.__format
1076 def getFile(self):
1077 return self.__file
1080 def getLocator( self ):
1081 """Gives back (Locator) a globaly unique identification encapsulated
1082 in a Locator object for the category instance
1084 loc = self.__conf.getLocator()
1085 loc["reviewingTemplateId"] = self.getId()
1086 return loc
1089 class ConferenceReviewingNotification(GenericNotification):
1090 """ Template to build an email notification to a newly appointed PRM / Referee / Editor / Reviewer / Abstract Manager / Abstract Reviewer
1093 def __init__(self, user, role, conference):
1094 GenericNotification.__init__(self)
1095 self.setFromAddr("Indico Mailer <%s>" % Config.getInstance().getNoReplyEmail())
1096 self.setToList([user.getEmail()])
1097 self.setSubject("""You have been chosen as a %s for the conference "%s" """
1098 % (role, conference.getTitle()))
1099 self.setBody("""Dear %s,
1101 You have been chosen as a %s for the conference entitled "%s" in order to help with the paper reviewing process. Please find the Paper Reviewing utilities here:
1105 Kind regards,
1106 Indico on behalf of "%s"
1107 """ % ( user.getStraightFullName(), role, conference.getTitle(), urlHandlers.UHPaperReviewingDisplay.getURL(conference), conference.getTitle()))
1109 class ConferenceReviewingRemoveNotification(GenericNotification):
1110 """ Template to build an email notification to a removed PRM / Referee / Editor / Reviewer / Abstract Manager / Abstract Reviewer
1113 def __init__(self, user, role, conference):
1114 GenericNotification.__init__(self)
1115 self.setFromAddr("Indico Mailer <%s>" % Config.getInstance().getNoReplyEmail())
1116 self.setToList([user.getEmail()])
1117 self.setSubject("""You are no longer a %s of the conference "%s" """
1118 % (role, conference.getTitle()))
1119 self.setBody("""Dear %s,
1121 Please, be aware that you are no longer a %s of the conference "%s":
1125 Kind regards,
1126 Indico on behalf of "%s"
1127 """% ( user.getStraightFullName(), role, conference.getTitle(), urlHandlers.UHConferenceDisplay.getURL(conference), conference.getTitle()))
1130 class Question(Persistent, Fossilizable):
1133 This class represents a question for the abstract/paper reviewing.
1136 fossilizes(IReviewingQuestionFossil)
1138 def __init__( self, newId, text):
1139 """ Constructor.
1140 text is a string which represents the content of the question
1142 self._id = newId
1143 self._text = text
1144 self._visible = True
1146 def getId(self):
1147 return self._id
1149 def getText(self):
1150 return self._text
1152 def setText(self, text):
1153 self._text = text
1156 class Answer(Persistent):
1159 This class represents an answer given of a question.
1162 def __init__(self, newId, rbValue, numberOfAnswers, question):
1163 """ Constructor.
1164 rbValue: real value of the radio button
1165 scaleLower, scaleHigher and numberOfAnswers: params to calculate the value in base to the scale and
1166 the number of answers
1167 question: Question objett associated
1169 self._id = newId
1170 self._rbValue = rbValue
1171 self._question = question
1172 # is necessary to save this value (_numberOfAnswers) here because when the value is recalculated in base to a new scale
1173 # we need to keep the rbValue in base to the previous number of answers, otherwise if the user changes the number of radio
1174 # buttons as well we could have for example rbValue = 5 and numberOfAnswers = 3 (values 1, 2, 3 but never 5)
1175 self._numberOfAnswers = numberOfAnswers
1176 self._value = None # value in base to the scale limits
1179 def getId(self):
1180 return self._id
1182 def getValue(self):
1183 return self._value
1185 def getQuestion(self):
1186 return self._question
1188 def getRbValue(self):
1189 return self._rbValue
1191 def setRbValue(self, rbValue):
1192 self._rbValue = rbValue
1194 def calculateRatingValue(self, scaleLower, scaleHigher):
1196 Calculate the value of the answer in base to the scale limits and the number of possible answers (radio buttons)
1198 self._value = (((scaleHigher-scaleLower)/float(self._numberOfAnswers-1))*self._rbValue) + scaleLower
1202 class Status(Persistent, Fossilizable):
1205 This class represents a status for the paper reviewing.
1208 fossilizes(IReviewingStatusFossil)
1210 def __init__( self, newId, name, editable):
1211 """ Constructor.
1212 name: is a string which represents the name of the status
1213 editable: is a boolean which represent if the status is going to be shown in the list for editing and removing options
1214 (default statuses)
1216 self._id = newId
1217 self._name = name
1218 self._editable = editable
1220 def getId(self):
1221 return self._id
1223 def getName(self):
1224 return self._name
1226 def setName(self, name):
1227 self._name = name
1229 def getEditable(self):
1230 return self._editable
1232 def setEditable(self, value):
1233 self._editable = value