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)} |
|