src/pyams_content/shared/common/review.py
changeset 272 c8e9ace0bf35
parent 131 b113d88d9ee8
equal deleted inserted replaced
271:d88bd7cdaebf 272:c8e9ace0bf35
    12 
    12 
    13 __docformat__ = 'restructuredtext'
    13 __docformat__ = 'restructuredtext'
    14 
    14 
    15 
    15 
    16 # import standard library
    16 # import standard library
    17 import os
       
    18 from datetime import datetime
       
    19 from uuid import uuid4
       
    20 
    17 
    21 # import interfaces
    18 # import interfaces
    22 from pyams_content.interfaces import READER_ROLE
       
    23 from pyams_content.interfaces.review import IReviewManager, IReviewComment, IReviewComments, \
       
    24     REVIEW_COMMENTS_ANNOTATION_KEY, CommentAddedEvent, ICommentAddedEvent
       
    25 from pyams_content.shared.common.interfaces import IWfSharedContent, IWfSharedContentRoles
       
    26 from pyams_i18n.interfaces import II18n
       
    27 from pyams_mail.interfaces import IPrincipalMailInfo
       
    28 from pyams_security.interfaces import ISecurityManager, IProtectedObject
       
    29 from pyams_security.interfaces.notification import INotificationSettings
       
    30 from pyramid_chameleon.interfaces import IChameleonTranslate
       
    31 from pyramid_mailer.interfaces import IMailer
       
    32 from zope.annotation.interfaces import IAnnotations
       
    33 from zope.location.interfaces import ISublocations
       
    34 from zope.traversing.interfaces import ITraversable
       
    35 
    19 
    36 # import packages
    20 # import packages
    37 from persistent import Persistent
       
    38 from pyams_mail.message import HTMLMessage
       
    39 from pyams_security.principal import MissingPrincipal
       
    40 from pyams_utils.adapter import adapter_config, ContextAdapter
       
    41 from pyams_utils.container import BTreeOrderedContainer
       
    42 from pyams_utils.registry import query_utility
       
    43 from pyams_utils.request import check_request, query_request
       
    44 from pyams_utils.url import absolute_url
       
    45 from pyramid.events import subscriber
       
    46 from pyramid.threadlocal import get_current_registry
       
    47 from pyramid_chameleon.zpt import PageTemplateFile
       
    48 from zope.container.contained import Contained
       
    49 from zope.interface import implementer
       
    50 from zope.location import locate
       
    51 from zope.schema.fieldproperty import FieldProperty
       
    52 
    21 
    53 from pyams_content import _
    22 # imports for backward compatibility: module moved to pyams_content.features.review !!
    54 
    23 from pyams_content.features.review import ReviewComment, ReviewCommentsContainer
    55 
       
    56 @implementer(IReviewComment)
       
    57 class ReviewComment(Persistent, Contained):
       
    58     """Review comment persistent class"""
       
    59 
       
    60     owner = FieldProperty(IReviewComment['owner'])
       
    61     comment = FieldProperty(IReviewComment['comment'])
       
    62     comment_type = FieldProperty(IReviewComment['comment_type'])
       
    63     creation_date = FieldProperty(IReviewComment['creation_date'])
       
    64 
       
    65     def __init__(self, owner, comment, comment_type='comment'):
       
    66         self.owner = owner
       
    67         self.comment = comment
       
    68         self.comment_type = comment_type
       
    69         self.creation_date = datetime.utcnow()
       
    70 
       
    71 
       
    72 @implementer(IReviewComments)
       
    73 class ReviewCommentsContainer(BTreeOrderedContainer):
       
    74     """Review comments container"""
       
    75 
       
    76     reviewers = FieldProperty(IReviewComments['reviewers'])
       
    77 
       
    78     def clear(self):
       
    79         for k in self.keys()[:]:
       
    80             del self[k]
       
    81 
       
    82     def add_comment(self, comment):
       
    83         uuid = str(uuid4())
       
    84         self[uuid] = comment
       
    85         reviewers = self.reviewers or set()
       
    86         reviewers.add(comment.owner)
       
    87         self.reviewers = reviewers
       
    88         get_current_registry().notify(CommentAddedEvent(self.__parent__, comment))
       
    89 
       
    90 
       
    91 @adapter_config(context=IWfSharedContent, provides=IReviewComments)
       
    92 def SharedContentReviewCommentsFactory(context):
       
    93     """Shared content review comments factory"""
       
    94     annotations = IAnnotations(context)
       
    95     comments = annotations.get(REVIEW_COMMENTS_ANNOTATION_KEY)
       
    96     if comments is None:
       
    97         comments = annotations[REVIEW_COMMENTS_ANNOTATION_KEY] = ReviewCommentsContainer()
       
    98         locate(comments, context, '++review-comments++')
       
    99     return comments
       
   100 
       
   101 
       
   102 @adapter_config(name='review-comments', context=IWfSharedContent, provides=ITraversable)
       
   103 class SharedContentReviewCommentsNamespace(ContextAdapter):
       
   104     """++review-comments++ namespace traverser"""
       
   105 
       
   106     def traverse(self, name, furtherpath=None):
       
   107         return IReviewComments(self.context)
       
   108 
       
   109 
       
   110 @adapter_config(name='review-comments', context=IWfSharedContent, provides=ISublocations)
       
   111 class SharedContentReviewCommentsSublocations(ContextAdapter):
       
   112     """Shared content review comments sub-location adapter"""
       
   113 
       
   114     def sublocations(self):
       
   115         return IReviewComments(self.context).values()
       
   116 
       
   117 
       
   118 @adapter_config(context=IWfSharedContent, provides=IReviewManager)
       
   119 class SharedContentReviewAdapter(ContextAdapter):
       
   120     """Shared content review adapter"""
       
   121 
       
   122     review_template = PageTemplateFile(os.path.join(os.path.dirname(__file__),
       
   123                                                     'zmi/templates/review-notification.pt'))
       
   124 
       
   125     def ask_review(self, reviewers, comment, notify_all=True):
       
   126         """Ask for content review"""
       
   127         roles = IWfSharedContentRoles(self.context, None)
       
   128         if roles is None:
       
   129             return
       
   130         # check request
       
   131         request = check_request()
       
   132         translate = request.localizer.translate
       
   133         # initialize mailer
       
   134         security = query_utility(ISecurityManager)
       
   135         settings = INotificationSettings(security)
       
   136         sender_name = request.principal.title if request.principal is not None else settings.sender_name
       
   137         sender_address = settings.sender_email
       
   138         sender = security.get_principal(request.principal.id, info=False)
       
   139         sender_mail_info = IPrincipalMailInfo(sender, None)
       
   140         if sender_mail_info is not None:
       
   141             for sender_name, sender_address in sender_mail_info.get_addresses():
       
   142                 break
       
   143         if settings.enable_notifications:
       
   144             mailer = query_utility(IMailer, name=settings.mailer)
       
   145         else:
       
   146             mailer = None
       
   147         # create message
       
   148         message_body = self.review_template(request=request,
       
   149                                             context=self.context,
       
   150                                             translate=query_utility(IChameleonTranslate),
       
   151                                             options={'settings': settings,
       
   152                                                      'comment': comment,
       
   153                                                      'sender': sender_name})
       
   154         # notify reviewers
       
   155         notifications = 0
       
   156         readers = roles.readers
       
   157         for reviewer in reviewers:
       
   158             if settings.enable_notifications and \
       
   159                     (mailer is not None) and \
       
   160                     (notify_all or (reviewer not in readers)):
       
   161                 principal = security.get_principal(reviewer, info=False)
       
   162                 if not isinstance(principal, MissingPrincipal):
       
   163                     mail_info = IPrincipalMailInfo(principal, None)
       
   164                     if mail_info is not None:
       
   165                         for name, address in mail_info.get_addresses():
       
   166                             message = HTMLMessage(
       
   167                                 subject=translate(_("[{service_name}] A content review is requested")).format(
       
   168                                     service_name=settings.subject_prefix),
       
   169                                 fromaddr='{name} <{address}>'.format(name=sender_name,
       
   170                                                                      address=sender_address),
       
   171                                 toaddr='{name} <{address}>'.format(name=name, address=address),
       
   172                                 html=message_body)
       
   173                             mailer.send(message)
       
   174                             notifications += 1
       
   175             readers.add(reviewer)
       
   176         roles.readers = readers
       
   177         # add comment
       
   178         review_comment = ReviewComment(owner=request.principal.id,
       
   179                                        comment=comment,
       
   180                                        comment_type='request')
       
   181         IReviewComments(self.context).add_comment(review_comment)
       
   182         # return notifications count
       
   183         return notifications
       
   184 
       
   185 
       
   186 #
       
   187 # Review comment notification
       
   188 #
       
   189 
       
   190 try:
       
   191     from pyams_notify.interfaces import INotification, INotificationHandler
       
   192     from pyams_notify.event import Notification
       
   193 except ImportError:
       
   194     pass
       
   195 else:
       
   196 
       
   197     @subscriber(ICommentAddedEvent)
       
   198     def handle_new_comment(event):
       
   199         """Handle new review comment"""
       
   200         request = query_request()
       
   201         if request is None:
       
   202             return
       
   203         content = event.object
       
   204         translate = request.localizer.translate
       
   205         notification = Notification(request=request,
       
   206                                     context=content,
       
   207                                     source=event.comment.owner,
       
   208                                     action='notify',
       
   209                                     category='content.review',
       
   210                                     message=translate(_("A new comment was added on content « {0} »")).format(
       
   211                                         II18n(content).query_attribute('title', request=request)),
       
   212                                     url=absolute_url(content, request, 'admin#review-comments.html'),
       
   213                                     comments=IReviewComments(content))
       
   214         notification.send()
       
   215 
       
   216 
       
   217     @adapter_config(name='content.review', context=INotification, provides=INotificationHandler)
       
   218     class ContentReviewNotificationHandler(ContextAdapter):
       
   219         """Content review notification handler"""
       
   220 
       
   221         def get_target(self):
       
   222             context = self.context.context
       
   223             principals = set()
       
   224             protection = IProtectedObject(context, None)
       
   225             if protection is not None:
       
   226                 principals |= protection.get_principals(READER_ROLE)
       
   227             comments = self.context.user_data.get('comments')
       
   228             if comments is not None:
       
   229                 principals |= comments.reviewers
       
   230             source_id = self.context.source['id']
       
   231             if source_id in principals:
       
   232                 principals.remove(source_id)
       
   233             return {'principals': tuple(principals)}