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 collections
import defaultdict
20 from MaKaC
.common
import log
21 from MaKaC
.webinterface
.mail
import GenericNotification
22 from MaKaC
.common
.info
import HelperMaKaCInfo
23 from MaKaC
.webinterface
import urlHandlers
24 from MaKaC
.common
.mail
import GenericMailer
25 from MaKaC
.common
.timezoneUtils
import getAdjustedDate
, nowutc
26 from persistent
import Persistent
27 from MaKaC
.errors
import MaKaCError
28 from MaKaC
.paperReviewing
import ConferencePaperReview
29 from indico
.core
.config
import Config
30 from MaKaC
.i18n
import _
31 from MaKaC
.fossils
.reviewing
import IReviewManagerFossil
,\
32 IReviewFossil
, IJudgementFossil
33 from MaKaC
.common
.fossilize
import fossilizes
, Fossilizable
34 from MaKaC
.paperReviewing
import Answer
35 from MaKaC
.common
.Counter
import Counter
36 from MaKaC
.webinterface
.urlHandlers
import UHContributionDisplay
, UHContributionReviewingJudgements
37 from indico
.util
.i18n
import ngettext
# unicode ngettext
39 ###############################################
40 # Contribution reviewing classes
41 ###############################################
43 class ReviewManager(Persistent
, Fossilizable
):
44 """ This class is the manager for the reviewing. It keeps historical of reviews on a contribution.
45 A ReviewManager object is always linked to only 1 contribution and vice-versa.
48 fossilizes(IReviewManagerFossil
)
50 def __init__( self
, contribution
):
52 contribution has to be a Contribution object (not an id)
54 self
._contribution
= contribution
#the parent contribution for this ReviewManager object
55 self
._reviewCounter
= 0 #the version number of the next new review (starts with 0)
56 self
._review
= Review(self
._reviewCounter
, self
) #the current review (a.k.a. last review), a Review object.
57 self
._versioning
= [self
._review
] # a list of reviews, including the present one
58 self
._referee
= None # the referee for this contribution. If None, it has not been assigned yet.
59 self
._editor
= None # the editor for this contribution. If None, it has not been assigned yet.
60 self
._reviewersList
= [] #the list of reviewers (there can be more than one) for this contribution.
62 def getContribution(self
):
63 """ Returns the parent contribution for this ReviewManager object
65 return self
._contribution
67 def getConference(self
):
68 """ Convenience method that returns the Conference object
69 to which the contribution belongs to.
71 return self
._contribution
.getConference()
73 def getConfPaperReview(self
):
74 """ Convenience method that returns the ConferencePaperReview object of the Conference
75 to which the contribution belongs to.
77 return self
.getConference().getConfPaperReview()
81 """ Creates a new Review object and saves it in the versioning list.
83 self
._reviewCounter
= self
._reviewCounter
+ 1
84 self
._review
= Review(self
._reviewCounter
, self
)
85 for reviewer
in self
._reviewersList
:
86 self
._review
.addReviewerJudgement(reviewer
)
87 self
._versioning
.append(self
._review
)
88 self
.notifyModification()
90 def getLastReview(self
):
91 """ Returns the current review as a Review object
95 def getVersioning(self
):
96 """ Returns the list of reviews, current and past, as a list of Review objects.
98 return self
._versioning
100 def getVersioningLen(self
):
101 return len(self
._versioning
)
103 def getReviewById(self
, reviewId
):
104 for i
in self
._versioning
:
105 if i
.getId() == int(reviewId
):
109 def getSortedVerioning(self
):
110 versioning
= self
._versioning
111 versioning
.sort(key
= lambda c
: c
.getId(), reverse
=True)
114 def isInReviewingTeamforContribution(self
, user
):
115 """ Returns if the user is in the reviewing team for this contribution
117 return self
.isReferee(user
) or \
118 self
.isEditor(user
) or \
119 self
.isReviewer(user
)
123 def getReferee(self
):
124 """ Returns the referee for this contribution
128 def setReferee(self
, referee
):
129 """ Sets the referee for this contribution.
130 referee has to be an Avatar object.
131 The referee is notified by an email.
133 if self
.hasReferee():
134 raise MaKaCError("This contribution already has a referee")
136 self
._referee
= referee
137 referee
.linkTo(self
._contribution
, "referee")
138 self
.getConfPaperReview().addRefereeContribution(referee
, self
._contribution
)
139 self
.getLastReview().setRefereeDueDate(self
.getConfPaperReview().getDefaultRefereeDueDate())
140 #e-mail notification will be send when referee is assigned to contribution only if the manager enable the option in 'Automatic e-mails' section
141 if self
.getConfPaperReview().getEnableRefereeEmailNotifForContribution():
142 notification
= ContributionReviewingNotification(referee
, 'Referee', self
._contribution
)
143 GenericMailer
.sendAndLog(notification
,
144 self
._contribution
.getConference(),
145 log
.ModuleNames
.PAPER_REVIEWING
)
147 def removeReferee(self
):
148 """ Removes the referee for this contribution.
149 There is no 'referee' argument because there is only 1 referee by contribution.
150 The ex-referee is notified by an email.
153 self
._referee
.unlinkTo(self
._contribution
, "referee")
154 self
.getConfPaperReview().removeRefereeContribution(self
._referee
, self
._contribution
)
157 if self
.hasReviewers():
158 self
.removeAllReviewers()
159 #e-mail notification will be send when referee is removed from contribution only if the manager enable the option in 'Automatic e-mails' section
160 if self
.getConfPaperReview().getEnableRefereeEmailNotifForContribution():
161 notification
= ContributionReviewingRemoveNotification(self
._referee
, 'Referee', self
._contribution
)
162 GenericMailer
.sendAndLog(notification
,
163 self
._contribution
.getConference(),
164 log
.ModuleNames
.PAPER_REVIEWING
)
167 def isReferee(self
, user
):
168 """ Returns if a given user is the referee for this contribution.
169 The user has to be an Avatar object.
171 return user
is not None and user
== self
._referee
173 def hasReferee(self
):
174 """ Returns if this conference has a referee already.
176 return self
._referee
is not None
180 """ Returns the editor for this contribution
184 def setEditor(self
, editor
):
185 """ Sets the editor for this contribution.
186 editor has to be an Avatar object.
187 The editor is notified by an email.
190 raise MaKaCError("This contribution is already has an editor")
191 elif self
.hasReferee() or self
.getConfPaperReview().getChoice() == ConferencePaperReview
.LAYOUT_REVIEWING
:
192 self
._editor
= editor
193 editor
.linkTo(self
._contribution
, "editor")
194 self
.getConfPaperReview().addEditorContribution(editor
, self
._contribution
)
195 self
.getLastReview().setEditorDueDate(self
.getConfPaperReview().getDefaultEditorDueDate())
196 #e-mail notification will be send when editor is assigned to contribution only if the manager enable the option in 'Automatic e-mails' section
197 if self
.getConfPaperReview().getEnableEditorEmailNotifForContribution():
198 notification
= ContributionReviewingNotification(editor
, 'Layout Reviewer', self
._contribution
)
199 GenericMailer
.sendAndLog(notification
,
200 self
._contribution
.getConference(),
201 log
.ModuleNames
.PAPER_REVIEWING
)
203 raise MaKaCError("Please choose a editor before assigning an editor")
205 def removeEditor(self
):
206 """ Removes the editor for this contribution.
207 There is no 'editor' argument because there is only 1 editor by contribution.
208 The ex-editor is notified by an email.
210 self
._editor
.unlinkTo(self
._contribution
, "editor")
211 self
.getConfPaperReview().removeEditorContribution(self
._editor
, self
._contribution
)
212 #e-mail notification will be send when editor is removed from contribution only if the manager enable the option in 'Automatic e-mails' section
213 if self
.getConfPaperReview().getEnableEditorEmailNotifForContribution():
214 notification
= ContributionReviewingRemoveNotification(self
._editor
, 'Layout Reviewer', self
._contribution
)
215 GenericMailer
.sendAndLog(notification
,
216 self
._contribution
.getConference(),
217 log
.ModuleNames
.PAPER_REVIEWING
)
220 def isEditor(self
, user
):
221 """ Returns if a given user is the editor for this contribution.
222 The user has to be an Avatar object.
224 return user
is not None and user
== self
._editor
227 """ Returns if this conference has a editor already.
229 return self
._editor
is not None
232 def addReviewer(self
, reviewer
):
233 """ Adds a reviewer to this contribution.
234 reviewer has to be an Avatar object.
236 if reviewer
in self
.getReviewersList():
237 raise MaKaCError("This contribution is already assigned to the chosen reviewer")
238 elif self
.hasReferee():
239 self
._reviewersList
.append(reviewer
)
240 self
.notifyModification()
241 reviewer
.linkTo(self
._contribution
, "reviewer")
242 self
.getConfPaperReview().addReviewerContribution(reviewer
, self
._contribution
)
243 self
.getLastReview().setReviewerDueDate(self
.getConfPaperReview().getDefaultReviewerDueDate())
244 if self
.getLastReview().getAdviceFrom(reviewer
) is None:
245 self
.getLastReview().addReviewerJudgement(reviewer
)
246 #e-mail notification will be send when reviewer is assigned to contribution only if the manager enable the option in 'Automatic e-mails' section
247 if self
.getConfPaperReview().getEnableReviewerEmailNotifForContribution():
248 notification
= ContributionReviewingNotification(reviewer
, 'Content Reviewer', self
._contribution
)
249 GenericMailer
.sendAndLog(notification
,
250 self
._contribution
.getConference(),
251 log
.ModuleNames
.PAPER_REVIEWING
)
253 raise MaKaCError("Please choose a referee before assigning a reviewer")
255 def removeReviewer(self
, reviewer
):
256 """ Removes the reviewer for this contribution.
257 The ex-reviewer is notified by an email.
259 if reviewer
in self
._reviewersList
:
260 reviewer
.unlinkTo(self
._contribution
, "reviewer")
261 self
.getConfPaperReview().removeReviewerContribution(reviewer
, self
._contribution
)
262 self
._reviewersList
.remove(reviewer
)
263 self
.notifyModification()
264 #e-mail notification will be send when reviewer is removed from contribution only if the manager enable the option in 'Automatic e-mails' section
265 if self
.getConfPaperReview().getEnableReviewerEmailNotifForContribution():
266 notification
= ContributionReviewingRemoveNotification(reviewer
, 'Content Reviewer', self
._contribution
)
267 GenericMailer
.sendAndLog(notification
,
268 self
._contribution
.getConference(),
269 log
.ModuleNames
.PAPER_REVIEWING
)
271 def removeAllReviewers(self
):
272 """ Removes all the reviewers for this contribution
274 for reviewer
in self
._reviewersList
:
275 reviewer
.unlinkTo(self
._contribution
, "reviewer")
276 self
.getConfPaperReview().removeReviewerContribution(reviewer
, self
._contribution
)
277 self
.notifyModification()
278 #e-mail notification will be send when reviewers are removed from contribution only if the manager enable the option in 'Automatic e-mails' section
279 if self
.getConfPaperReview().getEnableReviewerEmailNotifForContribution():
280 notification
= ContributionReviewingRemoveNotification(reviewer
, 'Content Reviewer', self
._contribution
)
281 GenericMailer
.sendAndLog(notification
,
282 self
._contribution
.getConference(),
283 log
.ModuleNames
.PAPER_REVIEWING
)
284 del(self
._reviewersList
[:])
286 def getReviewersList(self
):
287 """ Returns the list of reviewers of this contribution,
288 has a list of Avatar objects.
290 return self
._reviewersList
292 def isReviewer(self
, user
):
293 """ Returns if a given user is the reviewer for this contribution.
294 The user has to be an Avatar object.
296 return user
in self
._reviewersList
298 def hasReviewers(self
):
299 """ Returns if this conference has at least one reviewer already.
301 return len(self
._reviewersList
) > 0
303 def notifyModification(self
):
304 """ Notifies the DB that a list or dictionary attribute of this object has changed
309 class Judgement(Persistent
, Fossilizable
):
310 """ Parent class for RefereeJudgement, EditorJudgement and ReviewerJudgement
313 fossilizes(IJudgementFossil
)
315 def __init__(self
, review
, author
= None, judgement
= None, comments
= "", submitted
= False, submissionDate
= None):
316 self
._review
= review
#the parent Review object for this Judgement
317 self
._author
= author
#the user (Referee, Editor or Reviewer) author of the judgement
318 self
._judgement
= judgement
#the judgement is a status object, 1:Accept, 2:To be Corrected, 3:Reject, ...others
319 self
._comments
= comments
#the comments, a string
320 #a list with the Answers objects
322 self
._submitted
= submitted
#boolean that indicates if the judgement has been submitted or not
323 self
._submissionDate
= submissionDate
#the date where the judgement was passed
324 self
._answerCounter
= Counter(1)
329 def getReviewManager(self
):
330 return self
._review
.getReviewManager()
332 def getConfPaperReview(self
):
333 """ Convenience method that returns the ConferencePaperReview object of the Conference
334 to which the judgment belongs to.
336 return self
.getReviewManager().getConfPaperReview()
341 def getJudgement(self
):
342 if self
._judgement
== None:
345 return self
._judgement
.getName()
347 def getComments(self
):
348 return self
._comments
350 def getCommentsVerbose(self
):
352 if self
.getComments():
354 Please see the comments below from the reviewing team:
357 """%self
.getComments()
360 def getAnswers(self
):
361 """ To be implemented by sub-classes
365 def _getAnswerCounter(self
):
367 if self
._answerCounter
:
369 except AttributeError:
370 self
._answerCounter
= Counter(1)
371 return self
._answerCounter
374 def getNewAnswerId(self
):
375 """ Returns a new an unused answerId
376 Increments the answerId counter
378 return self
._getAnswerCounter
().newCount()
380 def getAllAnswers(self
):
383 def isSubmitted(self
):
384 return self
._submitted
386 def getSubmissionDate(self
):
387 """ Returns the submission date for the review
389 return self
._submissionDate
391 def getAdjustedSubmissionDate(self
):
392 """ Returns a timezone-aware submission date given the conference's timezone.
394 return getAdjustedDate(self
._submissionDate
, self
.getReviewManager().getConference())
396 def setAuthor(self
, user
):
399 def setJudgement(self
, judgementId
):
400 self
._judgement
= self
.getReviewManager().getConfPaperReview().getStatusById(judgementId
)
402 def setComments(self
, comments
):
403 self
._comments
= comments
405 def getAnswer(self
, questionId
):
406 """ Returns the Answer object if it already exists otherwise we create it
408 for answer
in self
._answers
:
409 if (questionId
== answer
.getQuestion().getId()):
413 def _getQuestionById(self
, questionId
):
414 return self
.getReviewManager().getConfPaperReview().getReviewingQuestionById(questionId
)
416 def createAnswer(self
, questionId
):
417 """ Create the new object with the initial value for the rbValue
419 newId
= self
.getNewAnswerId()
420 rbValue
= ConferencePaperReview
.initialSelectedAnswer
421 numberOfAnswers
= len(ConferencePaperReview
.reviewingQuestionsAnswers
)
422 question
= self
._getQuestionById
(questionId
)
423 newAnswer
= Answer(newId
, rbValue
, numberOfAnswers
, question
)
425 self
._answers
.append(newAnswer
)
426 except AttributeError:
428 self
._answers
.append(newAnswer
)
429 self
.notifyModification()
432 def setAnswer(self
, questionId
, rbValue
, numberOfAnswers
):
433 answer
= self
.getAnswer(questionId
)
435 answer
= self
.createAnswer(questionId
)
436 answer
.setRbValue(rbValue
)
438 def setSubmitted(self
, submitted
):
439 if self
._judgement
is None:
440 raise MaKaCError("Cannot submit an opinion without choosing the judgemenent before")
441 self
._submitted
= submitted
442 self
._submissionDate
= nowutc()
444 def purgeAnswers(self
):
445 """ Remove the answers of the questions that were sent but we don't need anymory because
446 the questions have been removed """
447 # Check if the question has been removed
448 for answer
in self
._answers
:
449 if (self
.getConfPaperReview().getReviewingQuestionById(answer
.getQuestion().getId()) == None):
450 self
._answers
.remove(answer
)
452 def sendNotificationEmail(self
, withdrawn
= False):
453 """ Sends an email to the contribution's authors when the referee, editor or reviewer
454 pass a judgement on the contribution and only if the manager has enabled the option in 'Automatic e-mails' section.
456 authorList
= self
.getReviewManager().getContribution().getSubmitterList()
457 referee
= self
.getReviewManager().getReferee()
458 for author
in authorList
:
459 if (isinstance(self
, RefereeJudgement
) and self
.getConfPaperReview().getEnableRefereeJudgementEmailNotif()) \
460 or (isinstance(self
, EditorJudgement
) and (self
._review
.getConference().getConfPaperReview().getChoice() == ConferencePaperReview
.LAYOUT_REVIEWING
or not self
.getJudgement() in ["Accept", "Reject"]) and self
.getConfPaperReview().getEnableEditorJudgementEmailNotif()) \
461 or (isinstance(self
, ReviewerJudgement
) and not self
.getJudgement() in ["Accept", "Reject"] and self
.getConfPaperReview().getEnableReviewerJudgementEmailNotif()):
463 notification
= ContributionReviewingJudgementWithdrawalNotification(author
, self
, self
.getReviewManager().getContribution())
465 notification
= ContributionReviewingJudgementNotification(author
, self
, self
.getReviewManager().getContribution())
466 GenericMailer
.sendAndLog(notification
,
467 self
._review
.getConference(),
468 log
.ModuleNames
.PAPER_REVIEWING
)
470 # We send an email to the Referee if the layout or the content reviewer has sent a judgement
472 if (self
.getConfPaperReview().getChoice() == 4 and isinstance(self
, EditorJudgement
) \
473 and self
.getConfPaperReview().getEnableEditorSubmittedRefereeEmailNotif()) \
474 or ((self
.getConfPaperReview().getChoice() == 2 or self
.getConfPaperReview().getChoice() == 4) and isinstance(self
, ReviewerJudgement
) \
475 and self
.getConfPaperReview().getEnableReviewerSubmittedRefereeEmailNotif()):
477 notification
= ContributionReviewingJudgementRefereeWithdrawalNotification(referee
, self
, self
.getReviewManager().getContribution())
479 notification
= ContributionReviewingJudgementRefereeNotification(referee
, self
, self
.getReviewManager().getContribution())
480 GenericMailer
.sendAndLog(notification
,
481 self
._review
.getConference(),
482 log
.ModuleNames
.PAPER_REVIEWING
)
484 def notifyModification(self
):
485 """ Notifies the DB that a list or dictionary attribute of this object has changed
490 class RefereeJudgement(Judgement
):
492 def setSubmitted(self
, submitted
):
493 """ Sets the final judgement for a review, since this is the Referee judgement.
494 The judgement is a string among the pre-defined states (Accept, Reject, To be corrected) and
495 any user-defined states.
496 If it's the first time that the final judgement is set for this review, the contribution materials
497 are copied into the review.
498 If the judgement is 'To be corrected' or one of the user-defined states, versioning takes place.
499 A new Review object is then created as 'last review'.
501 Judgement
.setSubmitted(self
, submitted
)
502 if (not self
._submitted
):
503 # Check if it is necessary to purge some answers
505 matReviewing
= self
.getReviewManager().getContribution().getReviewing()
507 self
.getReview().copyMaterials(matReviewing
)
509 # 2 --> to be corrected, > 3 has the same behaviour as 'to be corrected'
510 if int(self
._judgement
.getId()) == 2 or int(self
._judgement
.getId()) > 3:
511 rm
= self
.getReviewManager()
513 # remove reviewing materials from the contribution
514 rm
.getContribution().removeMaterial(matReviewing
)
517 def getAnswers(self
):
518 questionAnswerList
= []
519 for answer
in self
._answers
:
521 questionText
= answer
.getQuestion().getText()
522 questionJudgement
= ConferencePaperReview
.reviewingQuestionsAnswers
[answer
.getRbValue()]
523 questionAnswerList
.append(questionText
+": "+questionJudgement
)
524 except AttributeError:
526 return questionAnswerList
528 class EditorJudgement(Judgement
):
530 def setSubmitted(self
, submitted
):
531 """ Sets the final judgement for a review, if the reviewing mode is only layout reviewing
532 since this is the Layout Reviewer judgement.
533 The judgement is a string among the pre-defined states (Accept, Reject, To be corrected)
534 If it's the first time that the final judgement is set for this review, the contribution materials
535 are copied into the review.
536 If the judgement is 'To be corrected', versioning takes place.
537 A new Review object is then created as 'last review'.
539 Judgement
.setSubmitted(self
, submitted
)
540 if (not self
._submitted
):
541 # Check if it is necessary to purge some answers
543 # 1 --> Accepted, 2 --> To be corrected, 3 --> Rejected, >3 --> Custom, same behavour as To be corrected.
544 if self
.getReviewManager().getConference().getConfPaperReview().getChoice() == ConferencePaperReview
.LAYOUT_REVIEWING
and (self
._judgement
.getId() == "2" or int(self
._judgement
.getId()) > 3):
545 matReviewing
= self
.getReviewManager().getContribution().getReviewing()
546 self
.getReview().copyMaterials(matReviewing
)
547 rm
= self
.getReviewManager()
549 # remove reviewing materials from the contribution
550 rm
.getContribution().removeMaterial(matReviewing
)
552 def purgeAnswers(self
):
553 """ Remove the answers of the questions that were sent but we don't need anymory because
554 the questions have been removed """
555 # Check if the question has been removed
556 for answer
in self
._answers
:
557 if (self
.getConfPaperReview().getLayoutQuestionById(answer
.getQuestion().getId()) == None):
558 self
._answers
.remove(answer
)
560 def _getQuestionById(self
, questionId
):
561 return self
.getReviewManager().getConfPaperReview().getLayoutQuestionById(questionId
)
563 def getAnswers(self
):
564 questionAnswerList
= []
565 for answer
in self
._answers
:
567 questionText
= answer
.getQuestion().getText()
568 questionJudgement
= ConferencePaperReview
.reviewingQuestionsAnswers
[answer
.getRbValue()]
569 questionAnswerList
.append(questionText
+": "+questionJudgement
)
570 except AttributeError:
572 return questionAnswerList
575 class ReviewerJudgement(Judgement
):
577 def setSubmitted(self
, submitted
):
578 Judgement
.setSubmitted(self
, submitted
)
579 if (not self
._submitted
):
580 # Check if it is necessary to purge some answers
583 def getAnswers(self
):
584 questionAnswerList
= []
585 for answer
in self
._answers
:
587 questionText
= answer
.getQuestion().getText()
588 questionJudgement
= ConferencePaperReview
.reviewingQuestionsAnswers
[answer
.getRbValue()]
589 questionAnswerList
.append(questionText
+": "+questionJudgement
)
590 except AttributeError:
592 return questionAnswerList
595 class Review(Persistent
, Fossilizable
):
596 """This class represents the judgement of a contribution made by the referee. It contains judgement and comments
599 fossilizes(IReviewFossil
)
601 def __init__( self
, version
, reviewManager
):
602 """ Constructor for the class.
603 version: an integer, first version number is 0.
604 reviewManager: the parent ReviewManager object
606 self
._reviewManager
= reviewManager
#the parent ReviewManager object for this Review
607 self
._refereeJudgement
= RefereeJudgement(self
)
608 self
._editorJudgement
= EditorJudgement(self
)
609 self
._reviewerJudgements
= {}
611 self
._isAuthorSubmitted
= False #boolean that says if the author has submitted his / her materials or not
612 self
._version
= version
#the version number for this Review. Different Reviews for the same contribution have increasing version numbers.
613 self
._materials
= [] #'snapshot' of the materials that were analyzed by the reviewing team. Copied from the Contribution materials when judgement is passed.
614 self
._authorComments
= ""
615 self
._refereeDueDate
= None #the Deadline where the referee has to pass his/her judgement
616 self
._editorDueDate
= None #the Deadline where the editor has to pass his/her judgement
617 self
._reviewerDueDate
= None #the Deadline where all the reviewers have to pass his/her judgement
619 def notifyModification(self
, **kwargs
):
620 """ Notifies the DB that a list or dictionary attribute of this object has changed
624 def updateNonInheritingChildren(self
, element
, delete
=False):
628 """ Returns the id of this Review, which is the same as its version number
632 def getReviewManager(self
):
633 """ Returns the parent Review Manager object for this Review object.
635 return self
._reviewManager
637 def getContribution(self
):
638 """ Convenience method that returns the Contribution to which this Review belongs.
640 return self
._reviewManager
.getContribution()
642 def getConference(self
):
643 """ Convenience method that returns the Conference to which this Review belongs.
645 return self
._reviewManager
.getContribution().getConference()
647 def getConfPaperReview(self
):
648 """ Convenience method that returns the ConferencePaperReview object of the Conference
649 to which the contribution belongs to.
651 return self
.getConference().getConfPaperReview()
654 return self
.getContribution()
656 def getRefereeJudgement(self
):
657 return self
._refereeJudgement
659 def getEditorJudgement(self
):
660 return self
._editorJudgement
662 def getReviewerJudgement(self
, reviewer
):
663 return self
._reviewerJudgements
[reviewer
]
665 def _getReviewerStatus(self
, status
):
666 if self
.anyReviewerHasGivenAdvice():
667 advices
= defaultdict(int)
668 for reviewer
in self
._reviewManager
.getReviewersList():
669 judgement
= self
._reviewManager
.getLastReview().getReviewerJudgement(reviewer
).getJudgement()
670 if judgement
!= None:
671 advices
[judgement
] += 1
672 resume
= "(%s)" % ", ".join("%s %s" % (v
, k
.lower()) for k
, v
in advices
.iteritems())
673 status
.append(_("Content assessed by %s %s %s") % (
674 sum(advices
.values()), ngettext("reviewer", "reviewers", sum(advices
.values())), resume
))
676 status
.append(_("No content reviewers have decided yet"))
678 def getReviewingStatus(self
, forAuthor
= False):
679 """ Returns a list of strings with a description of the current status of the review.
682 if self
.isAuthorSubmitted():
683 if self
.getConfPaperReview().getChoice() == ConferencePaperReview
.LAYOUT_REVIEWING
:
684 if self
._editorJudgement
.isSubmitted():
685 status
.append(_("Assessed: ") + str(self
._editorJudgement
.getJudgement()))
687 status
.append(_("Pending layout reviewer decision"))
688 elif self
.getConfPaperReview().getChoice() == ConferencePaperReview
.CONTENT_AND_LAYOUT_REVIEWING
or self
.getConfPaperReview().getChoice() == ConferencePaperReview
.CONTENT_REVIEWING
:
689 if self
._refereeJudgement
.isSubmitted():
690 status
.append(_("Assessed: ") + str(self
._refereeJudgement
.getJudgement()))
692 status
.append(_("Pending referee decision"))
694 if self
.getConfPaperReview().getChoice() == ConferencePaperReview
.CONTENT_AND_LAYOUT_REVIEWING
:
695 editor
= self
._reviewManager
.getEditor()
696 if self
._reviewManager
.isEditor(editor
) and self
._editorJudgement
.isSubmitted():
697 status
.append(_("Layout assessed by ") + str(self
._reviewManager
.getEditor().getFullName())+ _(" as: ") + str(self
._editorJudgement
.getJudgement()))
699 status
.append(_("Pending layout reviewer decision"))
700 self
._getReviewerStatus
(status
)
701 if self
.getConfPaperReview().getChoice() == ConferencePaperReview
.CONTENT_REVIEWING
:
702 self
._getReviewerStatus
(status
)
704 status
.append(_("Materials not yet submitted"))
707 def isAuthorSubmitted(self
):
708 """ Returns if the author(s) of the contribution has marked the materials
710 When materials are marked as submitted, the review process can start.
712 return self
._isAuthorSubmitted
714 def setAuthorSubmitted(self
, submitted
):
715 """ If submitted is True, it means that the author has marked the materials as submitted.
716 If submitted is False, it means that the author has 'unmarked' the materials as submitted because
717 he/she did some mistakes.
718 In both cases, all the already chosen reviewing staff are notified with an email
719 only if the manager has enabled the option in 'Automatic e-mails' section.
722 self
._isAuthorSubmitted
= submitted
726 if self
._reviewManager
.hasReferee() and self
.getConfPaperReview().getEnableAuthorSubmittedMatRefereeEmailNotif():
727 notification
= MaterialsSubmittedNotification(self
._reviewManager
.getReferee(), 'Referee', self
._reviewManager
.getContribution())
728 GenericMailer
.sendAndLog(notification
,
729 self
._reviewManager
.getContribution().getConference(),
730 log
.ModuleNames
.PAPER_REVIEWING
)
732 if self
._reviewManager
.hasEditor() and self
.getConfPaperReview().getEnableAuthorSubmittedMatEditorEmailNotif():
733 notification
= MaterialsSubmittedNotification(self
._reviewManager
.getEditor(), 'Layout Reviewer', self
._reviewManager
.getContribution())
734 GenericMailer
.sendAndLog(notification
,
735 self
._reviewManager
.getContribution().getConference(),
736 log
.ModuleNames
.PAPER_REVIEWING
)
738 for reviewer
in self
._reviewManager
.getReviewersList():
739 if self
.getConfPaperReview().getEnableAuthorSubmittedMatReviewerEmailNotif():
740 notification
= MaterialsSubmittedNotification(reviewer
, 'Content Reviewer', self
._reviewManager
.getContribution())
741 GenericMailer
.sendAndLog(notification
,
742 self
._reviewManager
.getContribution().getConference(),
743 log
.ModuleNames
.PAPER_REVIEWING
)
745 def getVersion(self
):
746 """ Returns the version number for this review. The version number is an integer, starting by 0.
750 def setAuthorComments(self
, authorComments
):
751 self
._authorComments
= authorComments
753 def getAuthorComments(self
):
754 return self
._authorComments
757 #review materials methods, and methods necessary for material retrieval to work
758 def getMaterials(self
):
759 """ Returns the materials stored in this Review.
761 return self
._materials
763 def copyMaterials(self
, material
):
764 """ Copies the materials from the contribution to this review object.
765 This is done by cloning the materials, and putting the contribution as owner.
766 This way, even if the author deletes materials during another review, they endure in this review.
768 self
._materials
= [material
.clone(self
)]
770 def getMaterialById(self
, materialId
=0):
771 """ Returns one of the materials of the review given its id
773 So far there is just one material (reviewing) with many resources. So, by default we
774 get the first element of the list of materials.
776 return self
._materials
[int(materialId
)]
778 def getLocator(self
):
779 """Gives back a globaly unique identification encapsulated in a Locator
780 object for the Review instance
782 l
= self
.getOwner().getLocator()
783 l
["reviewId"] = self
.getId()
786 def isProtected(self
):
787 return self
.getOwner().isProtected()
789 def canIPAccess( self
, ip
):
790 return self
.getOwner().canIPAccess(ip
)
792 def canUserModify( self
, aw
):
793 return self
.getOwner().canUserModify(aw
)
796 def addReviewerJudgement(self
, reviewer
):#, questions, adviceJudgement, comments):
797 """ Adds an ReviewerJudgement object to the list of ReviewerJudgements objects of the review.
798 Each ReviewerJudgement object represents the opinion of a content reviewer.
800 self
._reviewerJudgements
[reviewer
] = ReviewerJudgement(self
, author
= reviewer
)
801 self
.notifyModification()
803 def hasGivenAdvice(self
, reviewer
):
804 """ Returns if a given user has given advice on the content of the contribution.
806 answer
= (reviewer
in self
._reviewerJudgements
) and (self
._reviewerJudgements
[reviewer
].isSubmitted())
809 def getAdviceFrom(self
, reviewer
):
810 """ Returns an the advice information from a given reviewer,
812 If the given reviewer doesn't have an Advice object yet, None is returned.
814 return self
._reviewerJudgements
.get(reviewer
, None)
816 def anyReviewerHasGivenAdvice(self
):
817 """ Returns if at least 1 reviewer has already given advice on the content of the contribution.
819 for reviewer
in self
._reviewManager
.getReviewersList():
820 if self
.hasGivenAdvice(reviewer
):
825 def allReviewersHaveGivenAdvice(self
):
826 """ Returns if all reviewers have already given advice on the content of the contribution.
828 for reviewer
in self
._reviewManager
.getReviewersList():
829 if not self
.hasGivenAdvice(reviewer
):
832 return len(self
._reviewManager
.getReviewersList()) > 0
834 def getReviewerJudgements(self
):
835 """ Returns the advices from the reviewers.
837 return self
._reviewerJudgements
.values()
839 def getSubmittedReviewerJudgement(self
):
840 """ Returns the advices from the reviewers, but only those that have been marked as submitted
842 return filter(lambda advice
:advice
.isSubmitted(), self
.getReviewerJudgements())
844 # #notification email methods
845 # def sendNotificationEmail(self, judgement):
846 # """ Sends an email to the contribution's authors when the referee, editor or reviewer
847 # pass a judgement on the contribution.
849 # authorList = self._reviewManager.getContribution().getAuthorList()
850 # for author in authorList:
851 # notification = ContributionReviewingJudgementNotification(author, judgement, self._reviewManager.getContribution())
852 # GenericMailer.sendAndLog(notification, self.getConference(), "Reviewing", author)
856 def setRefereeDueDate(self
, date
):
857 """ Sets the Deadline for the referee.
859 self
._refereeDueDate
= date
#datetime(year, month, day, 0, 0, 0, tzinfo=timezone(self.getConference().getTimezone()))
861 def getRefereeDueDate(self
):
862 """ Returns the Deadline for the referee
864 return self
._refereeDueDate
866 def getAdjustedRefereeDueDate(self
):
867 """ Returns a timezeone-aware Deadline for the referee given the conference's timezone.
869 if self
.getRefereeDueDate() is None:
872 return getAdjustedDate(self
._refereeDueDate
, self
.getConference())
874 def getAdjustedRefereeDueDateFormatted(self
):
875 """ Returns a timezeone-aware Deadline for the referee given the conference's timezone,
876 formatted to a string (this method is necessary due to syntax limitations of @Retrieve )
878 date
= self
.getAdjustedRefereeDueDate()
880 return datetime
.datetime
.strftime(date
,'%d/%m/%Y %H:%M')
884 def setEditorDueDate(self
, date
):
885 """ Sets the Deadline for the editor.
887 self
._editorDueDate
= date
#datetime(year, month, day, 0, 0, 0, tzinfo=timezone(self.getConference().getTimezone()))
889 def getEditorDueDate(self
):
890 """ Returns the Deadline for the editor
892 return self
._editorDueDate
894 def getAdjustedEditorDueDate(self
):
895 """ Returns a timezeone-aware Deadline for the editor given the conference's timezone.
897 if self
.getEditorDueDate() is None:
900 return getAdjustedDate(self
._editorDueDate
, self
.getConference())
902 def setReviewerDueDate(self
, date
):
903 """ Sets the Deadline for all the reviewers.
905 self
._reviewerDueDate
= date
#datetime(year, month, day, 0, 0, 0, tzinfo=timezone(self.getConference().getTimezone()))
907 def getReviewerDueDate(self
):
908 """ Returns the Deadline for all the reviewers.
910 return self
._reviewerDueDate
912 def getAdjustedReviewerDueDate(self
):
913 """ Returns a timezeone-aware Deadline for all the reviewers given the conference's timezone.
915 if self
.getReviewerDueDate() is None:
918 return getAdjustedDate(self
._reviewerDueDate
, self
.getConference())
920 def setModificationDate(self
):
921 """Update the modification date (of type 'datetime') to now."""
922 self
.modificationDate
= nowutc()
924 ######################################
925 # Email notification classes
926 ######################################
927 class ContributionReviewingNotification(GenericNotification
):
928 """ Template to build an email notification to a newly appointed PRM / Referee / Editor / Reviewer
929 for a given contribution.
932 def __init__(self
, user
, role
, contribution
):
933 GenericNotification
.__init
__(self
)
934 conference
= contribution
.getConference()
935 self
.setFromAddr("Indico <%s>" % Config
.getInstance().getNoReplyEmail())
936 self
.setToList([user
.getEmail()])
937 self
.setSubject("""You have been chosen as a %s for "%s" """% (role
, conference
.getTitle()))
939 if role
== 'Referee':
940 urlh
= urlHandlers
.UHConfModifListContribToJudge
941 elif role
== 'Layout Reviewer':
942 urlh
= urlHandlers
.UHConfModifListContribToJudgeAsEditor
943 elif role
== 'Content Reviewer':
944 urlh
= urlHandlers
.UHConfModifListContribToJudgeAsReviewer
946 self
.setBody("""Dear %s,
948 You have been chosen as a %s for the paper entitled "%s" (id: %s) for the conference "%s". Please find the %s utilities here:
953 Indico on behalf of "%s"
954 """ % ( user
.getStraightFullName(), role
, contribution
.getTitle(), str(contribution
.getId()),
955 conference
.getTitle(), role
, urlh
.getURL(contribution
), conference
.getTitle()))
957 class ContributionReviewingRemoveNotification(GenericNotification
):
958 """ Template to build an email notification to a removed PRM / Referee / Editor / Reviewer
959 for a given contribution.
962 def __init__(self
, user
, role
, contribution
):
963 GenericNotification
.__init
__(self
)
964 conference
= contribution
.getConference()
965 self
.setFromAddr("Indico <%s>" % Config
.getInstance().getNoReplyEmail())
966 self
.setToList([user
.getEmail()])
967 self
.setSubject("""You are no longer a %s of a paper for "%s" """ % (role
, conference
.getTitle()))
968 self
.setBody("""Dear %s,
970 Please, be aware that you are no longer a %s of the paper entitled "%s" (id: %s) for the conference "%s":
975 Indico on behalf of "%s"
976 """ % ( user
.getStraightFullName(), role
, contribution
.getTitle(), str(contribution
.getId()), conference
.getTitle(), str(urlHandlers
.UHConferenceDisplay
.getURL(conference
)), conference
.getTitle()))
978 class ContributionReviewingJudgementNotification(GenericNotification
):
979 """ Template to build an email notification for a contribution submitter
980 once the contribution has been judged
983 def __init__(self
, user
, judgement
, contribution
):
984 GenericNotification
.__init
__(self
)
985 conference
= contribution
.getConference()
986 self
.setFromAddr("Indico <%s>"%Config
.getInstance().getNoReplyEmail())
987 self
.setToList([user
.getEmail()])
988 self
.setBCCList([judgement
.getAuthor().getEmail()])
990 if isinstance(judgement
, EditorJudgement
):
991 if conference
.getConfPaperReview().getChoice() == ConferencePaperReview
.LAYOUT_REVIEWING
:
992 if judgement
.getJudgement() in ["Accept", "Reject"]:
993 self
.setAcceptedRejected(user
, judgement
, contribution
, conference
, "Layout Reviewer")
995 self
.setFullyReviewed(user
, judgement
, contribution
, conference
, "Layout Reviewer")
996 elif not judgement
.getJudgement() in ["Accept", "Reject"]:
997 self
.setPartiallyReviewed(user
, judgement
, contribution
, conference
, "Layout")
998 elif isinstance(judgement
, ReviewerJudgement
) and not judgement
.getJudgement() in ["Accept", "Reject"]:
999 self
.setPartiallyReviewed(user
, judgement
, contribution
, conference
, "Content")
1000 elif isinstance(judgement
, RefereeJudgement
):
1001 if judgement
.getJudgement() in ["Accept", "Reject"]:
1002 self
.setAcceptedRejected(user
, judgement
, contribution
, conference
, "Referee")
1004 self
.setFullyReviewed(user
, judgement
, contribution
, conference
, "Referee")
1006 def setFullyReviewed(self
, user
, judgement
, contribution
, conference
, role
):
1007 self
.setSubject("""Your paper "%s" for "%s" has been completely reviewed """
1008 % (contribution
.getTitle(), conference
.getTitle()))
1009 self
.setBody("""Dear %s,
1011 The %s has reviewed your paper entitled "%s" (id: %s), submitted for "%s".
1012 The assessment is as follows: %s.
1014 You may then apply the requested modifications to your paper and submit the modified version for review. In order to do so, please proceed to your paper page:
1019 Indico on behalf of "%s"
1020 """ % ( user
.getDirectFullNameNoTitle(upper
=False), role
, contribution
.getTitle(), str(contribution
.getId()),
1021 conference
.getTitle(), judgement
.getJudgement(), judgement
.getCommentsVerbose(), UHContributionDisplay
.getURL(contribution
), conference
.getTitle()))
1024 def setPartiallyReviewed(self
, user
, judgement
, contribution
, conference
, typeR
):
1025 self
.setSubject("""%s Assessment of your paper "%s" for "%s" """
1026 % (typeR
, contribution
.getTitle(), conference
.getTitle()))
1027 self
.setBody("""Dear %s,
1029 The assigned %s Reviewer has partially reviewed your paper entitled "%s" (id: %s) submitted for "%s".
1030 The assessment is as follows: %s.
1033 Note that this is a partial review, a final assessment will be done by the referee. In the meanwhile, you may access all the information about your paper from the following page:
1038 Indico on behalf of "%s"
1039 """ % ( user
.getDirectFullNameNoTitle(upper
=False),typeR
, contribution
.getTitle(), str(contribution
.getId()),
1040 conference
.getTitle(), judgement
.getJudgement(), judgement
.getCommentsVerbose(), UHContributionDisplay
.getURL(contribution
), conference
.getTitle()))
1042 def setAcceptedRejected(self
, user
, judgement
, contribution
, conference
, role
):
1043 if judgement
.getJudgement() == "Accept":
1044 judgementText
= "ACCEPTED"
1045 elif judgement
.getJudgement() == "Reject":
1046 judgementText
= "REJECTED"
1047 self
.setSubject("""Your paper "%s" for "%s" has been completely reviewed"""
1048 % (contribution
.getTitle(), conference
.getTitle()))
1049 self
.setBody("""Dear %s,
1051 The %s has %s your paper entitled "%s" (id: %s), submitted for "%s".
1053 You may proceed to your paper page:
1058 Indico on behalf of "%s"
1059 """ % ( user
.getDirectFullNameNoTitle(upper
=False), role
, judgementText
, contribution
.getTitle(), str(contribution
.getId()),
1060 conference
.getTitle(), judgement
.getCommentsVerbose(), UHContributionDisplay
.getURL(contribution
), conference
.getTitle()))
1062 class ContributionReviewingJudgementWithdrawalNotification(GenericNotification
):
1063 """ Template to build an email notification for a contribution submitter
1064 once the judgement of the contribution has been withdrawn.
1067 def __init__(self
, user
, judgement
, contribution
):
1068 GenericNotification
.__init
__(self
)
1069 conference
= contribution
.getConference()
1070 self
.setFromAddr("Indico <%s>" % Config
.getInstance().getNoReplyEmail())
1071 self
.setToList([user
.getEmail()])
1072 self
.setBCCList([judgement
.getAuthor().getEmail()])
1073 if isinstance(judgement
, RefereeJudgement
):
1075 elif isinstance(judgement
, EditorJudgement
):
1076 typeR
= "Layout Reviewer"
1077 elif isinstance(judgement
, ReviewerJudgement
):
1078 typeR
= "Content Reviewer"
1079 self
.setSubject(""""%s" has been put back into reviewing by the %s """ % (contribution
.getTitle(), typeR
))
1080 self
.setBody("""Dear %s,
1082 Your paper entitled "%s" (id: %s) submitted for "%s" has been put back into reviewing by the assigned %s:
1087 Indico on behalf of "%s"
1088 """ % ( user
.getDirectFullNameNoTitle(upper
=False), contribution
.getTitle(), str(contribution
.getId()),
1089 conference
.getTitle(), typeR
, urlHandlers
.UHContributionDisplay
.getURL(contribution
), conference
.getTitle())
1092 class ContributionReviewingJudgementRefereeNotification(GenericNotification
):
1093 """ Template to build an email notification for a referee
1094 once the contribution has been judged
1097 def __init__(self
, user
, judgement
, contribution
):
1098 GenericNotification
.__init
__(self
)
1099 conference
= contribution
.getConference()
1100 self
.setFromAddr("Indico <%s>"%Config
.getInstance().getNoReplyEmail())
1101 self
.setToList([user
.getEmail()])
1102 if isinstance(judgement
, EditorJudgement
):
1104 elif isinstance(judgement
, ReviewerJudgement
):
1107 self
.setSubject("""%s Assessment of the paper "%s" for "%s" """% (typeR
, contribution
.getTitle(), conference
.getTitle()))
1108 self
.setBody("""Dear %s,
1110 The assigned %s Reviewer, %s, has partially reviewed the paper entitled "%s" (id: %s) submitted for "%s".
1111 The assessment is as follows: %s.
1113 You may proceed to the Referee Area for this paper:
1118 Indico on behalf of "%s"
1119 """ % ( user
.getStraightFullName(), typeR
, judgement
.getAuthor().getStraightFullName(), contribution
.getTitle(),
1120 str(contribution
.getId()),conference
.getTitle(), judgement
.getJudgement(), judgement
.getCommentsVerbose(),
1121 UHContributionReviewingJudgements
.getURL(contribution
), conference
.getTitle()))
1123 class ContributionReviewingJudgementRefereeWithdrawalNotification(GenericNotification
):
1124 """ Template to build an email notification for a contribution submitter
1125 once the judgement of the contribution has been withdrawn.
1128 def __init__(self
, user
, judgement
, contribution
):
1129 GenericNotification
.__init
__(self
)
1130 conference
= contribution
.getConference()
1131 self
.setFromAddr("Indico <%s>" % Config
.getInstance().getNoReplyEmail())
1132 self
.setToList([user
.getEmail()])
1134 if isinstance(judgement
, EditorJudgement
):
1136 elif isinstance(judgement
, ReviewerJudgement
):
1138 self
.setSubject(""""%s" has been put back into reviewing by the %s Reviewer"""% (contribution
.getTitle(), typeR
))
1139 self
.setBody("""Dear %s,
1141 The paper entitled "%s" (id: %s) submitted for "%s" has been put back into reviewing by the assigned %s Reviewer:
1146 Indico on behalf of "%s"
1147 """ % ( user
.getStraightFullName(), contribution
.getTitle(), str(contribution
.getId()),conference
.getTitle(),
1148 typeR
, urlHandlers
.UHConfModifListContribToJudge
.getURL(contribution
), conference
.getTitle()))
1150 class MaterialsSubmittedNotification(GenericNotification
):
1152 def __init__(self
, user
, role
, contribution
):
1153 conference
= contribution
.getConference()
1154 GenericNotification
.__init
__(self
)
1155 self
.setFromAddr("Indico <%s>" % Config
.getInstance().getNoReplyEmail())
1156 self
.setToList([user
.getEmail()])
1157 self
.setSubject("""An author has submitted a paper for "%s" """% conference
.getTitle())
1159 if role
== 'Referee':
1160 urlh
= urlHandlers
.UHConfModifListContribToJudge
1161 elif role
== 'Layout Reviewer':
1162 urlh
= urlHandlers
.UHConfModifListContribToJudgeAsEditor
1163 elif role
== 'Content Reviewer':
1164 urlh
= urlHandlers
.UHConfModifListContribToJudgeAsReviewer
1166 self
.setBody("""Dear %s,
1168 An author has submitted a paper entitled "%s" (id: %s) for the conference "%s". You can now start the reviewing process as a %s:
1173 Indico on behalf of "%s"
1174 """ % ( user
.getStraightFullName(), contribution
.getTitle(), str(contribution
.getId()), conference
.getTitle(), role
, urlh
.getURL(contribution
), conference
.getTitle()))