Merge ~pappacena/launchpad:comment-editing-revisions-api into launchpad:master
- Git
- lp:~pappacena/launchpad
- comment-editing-revisions-api
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Merged |
---|---|
Approved by: | Thiago F. Pappacena |
Approved revision: | 23031fa129265a613dd4d47bff4ac95729c4f611 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~pappacena/launchpad:comment-editing-revisions-api |
Merge into: | launchpad:master |
Prerequisite: | ~pappacena/launchpad:comment-editing-api |
Diff against target: |
746 lines (+328/-54) 21 files modified
lib/lp/_schema_circular_imports.py (+2/-0) lib/lp/answers/browser/configure.zcml (+4/-0) lib/lp/answers/browser/question.py (+15/-0) lib/lp/answers/stories/webservice.txt (+1/-0) lib/lp/bugs/browser/bugcomment.py (+15/-0) lib/lp/bugs/browser/configure.zcml (+5/-1) lib/lp/code/browser/codereviewcomment.py (+15/-0) lib/lp/code/browser/configure.zcml (+4/-1) lib/lp/code/stories/webservice/xx-branchmergeproposal.txt (+3/-0) lib/lp/services/messages/browser/configure.zcml (+10/-0) lib/lp/services/messages/browser/message.py (+4/-0) lib/lp/services/messages/configure.zcml (+1/-0) lib/lp/services/messages/interfaces/message.py (+11/-1) lib/lp/services/messages/interfaces/messagerevision.py (+20/-6) lib/lp/services/messages/interfaces/webservice.py (+3/-1) lib/lp/services/messages/model/message.py (+11/-4) lib/lp/services/messages/model/messagerevision.py (+21/-0) lib/lp/services/messages/tests/scenarios.py (+41/-0) lib/lp/services/messages/tests/test_message.py (+16/-35) lib/lp/services/messages/tests/test_messagerevision.py (+120/-5) lib/lp/services/webservice/wadl-to-refhtml.xsl (+6/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+402285@code.launchpad.net |
Commit message
API to get and delete comment's revision history for bug messages, answers and code review comments
Description of the change
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Revision history for this message
Thiago F. Pappacena (pappacena) : | # |
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py |
2 | index a61d39d..4b1ecff 100644 |
3 | --- a/lib/lp/_schema_circular_imports.py |
4 | +++ b/lib/lp/_schema_circular_imports.py |
5 | @@ -149,6 +149,7 @@ from lp.services.messages.interfaces.message import ( |
6 | IMessage, |
7 | IUserToUserEmail, |
8 | ) |
9 | +from lp.services.messages.interfaces.messagerevision import IMessageRevision |
10 | from lp.services.webservice.apihelpers import ( |
11 | patch_collection_property, |
12 | patch_collection_return_type, |
13 | @@ -612,6 +613,7 @@ patch_reference_property(IIndexedMessage, 'inside', IBugTask) |
14 | |
15 | # IMessage |
16 | patch_reference_property(IMessage, 'owner', IPerson) |
17 | +patch_collection_property(IMessage, 'revisions', IMessageRevision) |
18 | |
19 | # IUserToUserEmail |
20 | patch_reference_property(IUserToUserEmail, 'sender', IPerson) |
21 | diff --git a/lib/lp/answers/browser/configure.zcml b/lib/lp/answers/browser/configure.zcml |
22 | index 631e8b8..87a6b76 100644 |
23 | --- a/lib/lp/answers/browser/configure.zcml |
24 | +++ b/lib/lp/answers/browser/configure.zcml |
25 | @@ -282,6 +282,10 @@ |
26 | module=".question" |
27 | classes="QuestionNavigation" |
28 | /> |
29 | + <browser:navigation |
30 | + module=".question" |
31 | + classes="QuestionMessageNavigation" |
32 | + /> |
33 | |
34 | <browser:url |
35 | for="lp.answers.interfaces.questioncollection.IQuestionSet" |
36 | diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py |
37 | index 814f329..68c4b59 100644 |
38 | --- a/lib/lp/answers/browser/question.py |
39 | +++ b/lib/lp/answers/browser/question.py |
40 | @@ -71,6 +71,7 @@ from lp.answers.interfaces.question import ( |
41 | IQuestionLinkFAQForm, |
42 | ) |
43 | from lp.answers.interfaces.questioncollection import IQuestionSet |
44 | +from lp.answers.interfaces.questionmessage import IQuestionMessage |
45 | from lp.answers.interfaces.questiontarget import ( |
46 | IAnswersFrontPageSearchForm, |
47 | IQuestionTarget, |
48 | @@ -275,6 +276,20 @@ class QuestionNavigation(Navigation): |
49 | return None |
50 | |
51 | |
52 | +class QuestionMessageNavigation(Navigation): |
53 | + """Navigation for the IQuestionMessage.""" |
54 | + |
55 | + usedfor = IQuestionMessage |
56 | + |
57 | + @stepthrough('revisions') |
58 | + def traverse_revisions(self, revision): |
59 | + try: |
60 | + revision = int(revision) |
61 | + except ValueError: |
62 | + return None |
63 | + return self.context.getRevisionByNumber(revision) |
64 | + |
65 | + |
66 | class QuestionBreadcrumb(Breadcrumb): |
67 | """Builds a breadcrumb for an `IQuestion`.""" |
68 | |
69 | diff --git a/lib/lp/answers/stories/webservice.txt b/lib/lp/answers/stories/webservice.txt |
70 | index 8ab277c..0954ced 100644 |
71 | --- a/lib/lp/answers/stories/webservice.txt |
72 | +++ b/lib/lp/answers/stories/webservice.txt |
73 | @@ -242,6 +242,7 @@ that indicate how the message changed the question. |
74 | parent_link: None |
75 | question_link: 'http://api.launchpad.test/devel/my-project/+question/...' |
76 | resource_type_link: 'http://api.launchpad.test/devel/#question_message' |
77 | + revisions_collection_link: 'http://...' |
78 | self_link: |
79 | 'http://api.launchpad.test/devel/my-project/+question/.../messages/1' |
80 | subject: 'Re: Q 1 great' |
81 | diff --git a/lib/lp/bugs/browser/bugcomment.py b/lib/lp/bugs/browser/bugcomment.py |
82 | index e40967f..6cc89f2 100644 |
83 | --- a/lib/lp/bugs/browser/bugcomment.py |
84 | +++ b/lib/lp/bugs/browser/bugcomment.py |
85 | @@ -49,6 +49,8 @@ from lp.services.propertycache import ( |
86 | from lp.services.webapp import ( |
87 | canonical_url, |
88 | LaunchpadView, |
89 | + Navigation, |
90 | + stepthrough, |
91 | ) |
92 | from lp.services.webapp.breadcrumb import Breadcrumb |
93 | from lp.services.webapp.interfaces import ILaunchBag |
94 | @@ -57,6 +59,19 @@ from lp.services.webapp.interfaces import ILaunchBag |
95 | COMMENT_ACTIVITY_GROUPING_WINDOW = timedelta(minutes=5) |
96 | |
97 | |
98 | +class BugCommentNavigation(Navigation): |
99 | + """Navigation for the `IBugComment`.""" |
100 | + usedfor = IBugComment |
101 | + |
102 | + @stepthrough('revisions') |
103 | + def traverse_revisions(self, revision): |
104 | + try: |
105 | + revision = int(revision) |
106 | + except ValueError: |
107 | + return None |
108 | + return self.context.getRevisionByNumber(revision) |
109 | + |
110 | + |
111 | def build_comments_from_chunks( |
112 | bugtask, truncate=False, slice_info=None, show_spam_controls=False, |
113 | user=None, hide_first=False): |
114 | diff --git a/lib/lp/bugs/browser/configure.zcml b/lib/lp/bugs/browser/configure.zcml |
115 | index ac80819..d9c36a8 100644 |
116 | --- a/lib/lp/bugs/browser/configure.zcml |
117 | +++ b/lib/lp/bugs/browser/configure.zcml |
118 | @@ -1,4 +1,4 @@ |
119 | -<!-- Copyright 2010-2014 Canonical Ltd. This software is licensed under the |
120 | +<!-- Copyright 2010-2021 Canonical Ltd. This software is licensed under the |
121 | GNU Affero General Public License version 3 (see the file LICENSE). |
122 | --> |
123 | |
124 | @@ -163,6 +163,10 @@ |
125 | path_expression="string:comments/${index}" |
126 | attribute_to_parent="bugtask" |
127 | rootsite="bugs"/> |
128 | + <browser:navigation |
129 | + module=".bugcomment" |
130 | + classes="BugCommentNavigation" |
131 | + /> |
132 | <browser:page |
133 | for="lp.bugs.interfaces.bugmessage.IBugComment" |
134 | name="+index" |
135 | diff --git a/lib/lp/code/browser/codereviewcomment.py b/lib/lp/code/browser/codereviewcomment.py |
136 | index 0d0496d..094cd79 100644 |
137 | --- a/lib/lp/code/browser/codereviewcomment.py |
138 | +++ b/lib/lp/code/browser/codereviewcomment.py |
139 | @@ -56,10 +56,25 @@ from lp.services.webapp import ( |
140 | ContextMenu, |
141 | LaunchpadView, |
142 | Link, |
143 | + Navigation, |
144 | + stepthrough, |
145 | ) |
146 | from lp.services.webapp.interfaces import ILaunchBag |
147 | |
148 | |
149 | +class CodeReviewCommentNavigation(Navigation): |
150 | + """Navigation for the `ICodeReviewComment`.""" |
151 | + usedfor = ICodeReviewComment |
152 | + |
153 | + @stepthrough('revisions') |
154 | + def traverse_revisions(self, revision): |
155 | + try: |
156 | + revision = int(revision) |
157 | + except ValueError: |
158 | + return None |
159 | + return self.context.getRevisionByNumber(int(revision)) |
160 | + |
161 | + |
162 | class ICodeReviewDisplayComment(IComment, ICodeReviewComment): |
163 | """Marker interface for displaying code review comments.""" |
164 | message = Object(schema=IMessage, title=_('The message.')) |
165 | diff --git a/lib/lp/code/browser/configure.zcml b/lib/lp/code/browser/configure.zcml |
166 | index e7943c0..f7bab39 100644 |
167 | --- a/lib/lp/code/browser/configure.zcml |
168 | +++ b/lib/lp/code/browser/configure.zcml |
169 | @@ -1,4 +1,4 @@ |
170 | -<!-- Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
171 | +<!-- Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
172 | GNU Affero General Public License version 3 (see the file LICENSE). |
173 | --> |
174 | |
175 | @@ -556,6 +556,9 @@ |
176 | path_expression="string:comments/${id}" |
177 | attribute_to_parent="branch_merge_proposal" |
178 | rootsite="code"/> |
179 | + <browser:navigation |
180 | + module=".codereviewcomment" |
181 | + classes="CodeReviewCommentNavigation" /> |
182 | <browser:defaultView |
183 | for="lp.code.interfaces.codereviewcomment.ICodeReviewComment" |
184 | name="+index"/> |
185 | diff --git a/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt b/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt |
186 | index 58b86e7..ccb1c9d 100644 |
187 | --- a/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt |
188 | +++ b/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt |
189 | @@ -206,6 +206,7 @@ The comments on a branch merge proposal are exposed through the API. |
190 | message_body: 'This is great work' |
191 | owner_link: 'http://...' |
192 | resource_type_link: 'http://.../#code_review_comment' |
193 | + revisions_collection_link: 'http://...' |
194 | self_link: 'http://.../~source/fooix/fix-it/+merge/.../comments/...' |
195 | title: 'Comment on proposed merge of lp://dev/~source/fooix/fix-it into lp://dev/~target/fooix/trunk' |
196 | vote: 'Approve' |
197 | @@ -228,6 +229,7 @@ The comments on a branch merge proposal are exposed through the API. |
198 | message_body: 'This is mediocre work.' |
199 | owner_link: 'http://...' |
200 | resource_type_link: 'http://.../#code_review_comment' |
201 | + revisions_collection_link: 'http://...' |
202 | self_link: 'http://.../~source/fooix/fix-it/+merge/.../comments/...' |
203 | title: ... |
204 | vote: 'Abstain' |
205 | @@ -306,6 +308,7 @@ Now the code review should be made. |
206 | message_body: 'This is great work' |
207 | owner_link: 'http://...' |
208 | resource_type_link: 'http://.../#code_review_comment' |
209 | + revisions_collection_link: 'http://...' |
210 | self_link: 'http://.../~source/fooix/fix-it/+merge/.../comments/...' |
211 | title: ... |
212 | vote: 'Approve' |
213 | diff --git a/lib/lp/services/messages/browser/configure.zcml b/lib/lp/services/messages/browser/configure.zcml |
214 | new file mode 100644 |
215 | index 0000000..0413ba9 |
216 | --- /dev/null |
217 | +++ b/lib/lp/services/messages/browser/configure.zcml |
218 | @@ -0,0 +1,10 @@ |
219 | +<configure |
220 | + xmlns="http://namespaces.zope.org/zope" |
221 | + xmlns:browser="http://namespaces.zope.org/browser" |
222 | + xmlns:i18n="http://namespaces.zope.org/i18n" |
223 | + i18n_domain="launchpad"> |
224 | + <browser:url |
225 | + for="lp.services.messages.interfaces.messagerevision.IMessageRevision" |
226 | + path_expression="string:revisions/${revision}" |
227 | + attribute_to_parent="message_implementation" /> |
228 | +</configure> |
229 | diff --git a/lib/lp/services/messages/browser/message.py b/lib/lp/services/messages/browser/message.py |
230 | index 06800e6..1e4a6e7 100644 |
231 | --- a/lib/lp/services/messages/browser/message.py |
232 | +++ b/lib/lp/services/messages/browser/message.py |
233 | @@ -7,6 +7,7 @@ __metaclass__ = type |
234 | |
235 | from zope.interface import implementer |
236 | |
237 | +from lp.bugs.interfaces.bugmessage import IBugMessage |
238 | from lp.services.messages.interfaces.message import IIndexedMessage |
239 | from lp.services.webapp.interfaces import ICanonicalUrlData |
240 | |
241 | @@ -28,6 +29,9 @@ class BugMessageCanonicalUrlData: |
242 | |
243 | def __init__(self, bug, message): |
244 | self.inside = bug.default_bugtask |
245 | + if IBugMessage.providedBy(message): |
246 | + # bug.messages is a list of Message objects, not BugMessage. |
247 | + message = message.message |
248 | self.path = "comments/%d" % list(bug.messages).index(message) |
249 | |
250 | |
251 | diff --git a/lib/lp/services/messages/configure.zcml b/lib/lp/services/messages/configure.zcml |
252 | index 19cb3c3..867b260 100644 |
253 | --- a/lib/lp/services/messages/configure.zcml |
254 | +++ b/lib/lp/services/messages/configure.zcml |
255 | @@ -82,4 +82,5 @@ |
256 | /> |
257 | |
258 | <webservice:register module="lp.services.messages.interfaces.webservice" /> |
259 | + <include package=".browser"/> |
260 | </configure> |
261 | diff --git a/lib/lp/services/messages/interfaces/message.py b/lib/lp/services/messages/interfaces/message.py |
262 | index 6d4e186..598b665 100644 |
263 | --- a/lib/lp/services/messages/interfaces/message.py |
264 | +++ b/lib/lp/services/messages/interfaces/message.py |
265 | @@ -88,7 +88,14 @@ class IMessageCommon(Interface): |
266 | Reference(title=_('Person'), schema=Interface, |
267 | required=False, readonly=True)) |
268 | |
269 | - revisions = Attribute(_('Message revision history')) |
270 | + revisions = exported(CollectionField( |
271 | + title=_("Message revision history"), |
272 | + description=_( |
273 | + "Revision history of this message, sorted in ascending order."), |
274 | + # Really IMessageRevision, patched in _schema_circular_imports. |
275 | + value_type=Reference(schema=Interface), |
276 | + required=False, readonly=True), as_of="devel") |
277 | + |
278 | datecreated = exported( |
279 | Datetime(title=_('Date Created'), required=True, readonly=True), |
280 | exported_as='date_created') |
281 | @@ -100,6 +107,9 @@ class IMessageCommon(Interface): |
282 | title=_('When this message was deleted'), required=False, |
283 | readonly=True)) |
284 | |
285 | + def getRevisionByNumber(revision_number): |
286 | + """Returns the revision with the given number.""" |
287 | + |
288 | |
289 | class IMessageView(IMessageCommon): |
290 | """Public attributes for message. |
291 | diff --git a/lib/lp/services/messages/interfaces/messagerevision.py b/lib/lp/services/messages/interfaces/messagerevision.py |
292 | index 49d3b9f..3ee5a90 100644 |
293 | --- a/lib/lp/services/messages/interfaces/messagerevision.py |
294 | +++ b/lib/lp/services/messages/interfaces/messagerevision.py |
295 | @@ -10,6 +10,12 @@ __all__ = [ |
296 | 'IMessageRevisionChunk', |
297 | ] |
298 | |
299 | +from lazr.restful.declarations import ( |
300 | + export_write_operation, |
301 | + exported, |
302 | + exported_as_webservice_entry, |
303 | + operation_for_version, |
304 | + ) |
305 | from lazr.restful.fields import Reference |
306 | from zope.interface import ( |
307 | Attribute, |
308 | @@ -31,21 +37,26 @@ class IMessageRevisionView(Interface): |
309 | |
310 | revision = Int(title=_("Revision number"), required=True, readonly=True) |
311 | |
312 | - content = Text( |
313 | + content = exported(Text( |
314 | title=_("The message at the given revision"), |
315 | - required=True, readonly=True) |
316 | + required=True, readonly=True)) |
317 | |
318 | message = Reference( |
319 | title=_('The current message of this revision.'), |
320 | schema=IMessage, required=True, readonly=True) |
321 | |
322 | - date_created = Datetime( |
323 | + message_implementation = Reference( |
324 | + title=_('The message implementation (BugComment, QuestionMessage or ' |
325 | + 'CodeReviewComment) related to this revision'), |
326 | + schema=IMessage, required=True, readonly=True) |
327 | + |
328 | + date_created = exported(Datetime( |
329 | title=_("The time when this message revision was created."), |
330 | - required=True, readonly=True) |
331 | + required=True, readonly=True)) |
332 | |
333 | - date_deleted = Datetime( |
334 | + date_deleted = exported(Datetime( |
335 | title=_("The time when this message revision was created."), |
336 | - required=False, readonly=True) |
337 | + required=False, readonly=True)) |
338 | |
339 | chunks = Attribute(_('Message pieces')) |
340 | |
341 | @@ -53,10 +64,13 @@ class IMessageRevisionView(Interface): |
342 | class IMessageRevisionEdit(Interface): |
343 | """IMessageRevision editable attributes.""" |
344 | |
345 | + @export_write_operation() |
346 | + @operation_for_version("devel") |
347 | def deleteContent(): |
348 | """Deletes this MessageRevision content.""" |
349 | |
350 | |
351 | +@exported_as_webservice_entry(publish_web_link=False, as_of="devel") |
352 | class IMessageRevision(IMessageRevisionView, IMessageRevisionEdit): |
353 | """A historical revision of a IMessage.""" |
354 | |
355 | diff --git a/lib/lp/services/messages/interfaces/webservice.py b/lib/lp/services/messages/interfaces/webservice.py |
356 | index 94960a9..1dade00 100644 |
357 | --- a/lib/lp/services/messages/interfaces/webservice.py |
358 | +++ b/lib/lp/services/messages/interfaces/webservice.py |
359 | @@ -1,4 +1,4 @@ |
360 | -# Copyright 2011 Canonical Ltd. This software is licensed under the |
361 | +# Copyright 2011-2021 Canonical Ltd. This software is licensed under the |
362 | # GNU Affero General Public License version 3 (see the file LICENSE). |
363 | |
364 | """All the interfaces that are exposed through the webservice. |
365 | @@ -12,10 +12,12 @@ which tells `lazr.restful` that it should look for webservice exports here. |
366 | __metaclass__ = type |
367 | __all__ = [ |
368 | 'IMessage', |
369 | + 'IMessageRevision', |
370 | ] |
371 | |
372 | from lp import _schema_circular_imports |
373 | from lp.services.messages.interfaces.message import IMessage |
374 | +from lp.services.messages.interfaces.messagerevision import IMessageRevision |
375 | |
376 | |
377 | _schema_circular_imports |
378 | diff --git a/lib/lp/services/messages/model/message.py b/lib/lp/services/messages/model/message.py |
379 | index d20ca2f..f6638b2 100644 |
380 | --- a/lib/lp/services/messages/model/message.py |
381 | +++ b/lib/lp/services/messages/model/message.py |
382 | @@ -174,13 +174,20 @@ class Message(SQLBase): |
383 | """See `IMessage`.""" |
384 | return None |
385 | |
386 | + @property |
387 | + def _revisions(self): |
388 | + return Store.of(self).find( |
389 | + MessageRevision, |
390 | + MessageRevision.message == self |
391 | + ).order_by(MessageRevision.revision) |
392 | + |
393 | @cachedproperty |
394 | def revisions(self): |
395 | """See `IMessage`.""" |
396 | - return list(Store.of(self).find( |
397 | - MessageRevision, |
398 | - MessageRevision.message == self |
399 | - ).order_by(MessageRevision.revision)) |
400 | + return list(self._revisions) |
401 | + |
402 | + def getRevisionByNumber(self, revision_number): |
403 | + return self._revisions.find(revision=revision_number).one() |
404 | |
405 | def editContent(self, new_content): |
406 | """See `IMessage`.""" |
407 | diff --git a/lib/lp/services/messages/model/messagerevision.py b/lib/lp/services/messages/model/messagerevision.py |
408 | index f4a1466..535ff66 100644 |
409 | --- a/lib/lp/services/messages/model/messagerevision.py |
410 | +++ b/lib/lp/services/messages/model/messagerevision.py |
411 | @@ -57,6 +57,27 @@ class MessageRevision(StormBase): |
412 | self.date_created = date_created |
413 | self.date_deleted = date_deleted |
414 | |
415 | + @property |
416 | + def message_implementation(self): |
417 | + from lp.bugs.model.bugmessage import BugMessage |
418 | + from lp.code.model.codereviewcomment import CodeReviewComment |
419 | + from lp.answers.model.questionmessage import QuestionMessage |
420 | + |
421 | + store = IStore(self) |
422 | + (identifier, ) = store.execute(""" |
423 | + SELECT 'bug' FROM BugMessage WHERE message = %s |
424 | + UNION |
425 | + SELECT 'question' FROM QuestionMessage WHERE message = %s |
426 | + UNION |
427 | + SELECT 'mp' FROM CodeReviewMessage WHERE message = %s; |
428 | + """, params=[self.message_id] * 3).get_one() |
429 | + id_to_class = { |
430 | + "bug": BugMessage, |
431 | + "question": QuestionMessage, |
432 | + "mp": CodeReviewComment} |
433 | + klass = id_to_class[identifier] |
434 | + return store.find(klass, klass.message == self.message_id).one() |
435 | + |
436 | @cachedproperty |
437 | def chunks(self): |
438 | return list(IStore(self).find( |
439 | diff --git a/lib/lp/services/messages/tests/scenarios.py b/lib/lp/services/messages/tests/scenarios.py |
440 | new file mode 100644 |
441 | index 0000000..f366574 |
442 | --- /dev/null |
443 | +++ b/lib/lp/services/messages/tests/scenarios.py |
444 | @@ -0,0 +1,41 @@ |
445 | +# Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
446 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
447 | + |
448 | +__metaclass__ = type |
449 | + |
450 | +from testscenarios import WithScenarios |
451 | +from zope.security.proxy import ProxyFactory |
452 | + |
453 | +from lp.bugs.model.bugmessage import BugMessage |
454 | +from lp.services.database.interfaces import IStore |
455 | +from lp.testing import ( |
456 | + login_person, |
457 | + ) |
458 | + |
459 | + |
460 | +class MessageTypeScenariosMixin(WithScenarios): |
461 | + |
462 | + scenarios = [ |
463 | + ("bug", {"message_type": "bug"}), |
464 | + ("question", {"message_type": "question"}), |
465 | + ("MP comment", {"message_type": "mp"}) |
466 | + ] |
467 | + |
468 | + def setUp(self): |
469 | + super(MessageTypeScenariosMixin, self).setUp() |
470 | + self.person = self.factory.makePerson() |
471 | + login_person(self.person) |
472 | + |
473 | + def makeMessage(self, content=None, **kwargs): |
474 | + owner = kwargs.pop('owner', self.person) |
475 | + if self.message_type == "bug": |
476 | + msg = self.factory.makeBugComment( |
477 | + owner=owner, body=content, **kwargs) |
478 | + return ProxyFactory(IStore(BugMessage).find( |
479 | + BugMessage, BugMessage.message == msg).one()) |
480 | + elif self.message_type == "question": |
481 | + question = self.factory.makeQuestion() |
482 | + return question.giveAnswer(owner, content) |
483 | + elif self.message_type == "mp": |
484 | + return self.factory.makeCodeReviewComment( |
485 | + sender=owner, body=content) |
486 | diff --git a/lib/lp/services/messages/tests/test_message.py b/lib/lp/services/messages/tests/test_message.py |
487 | index 070bc91..c483305 100644 |
488 | --- a/lib/lp/services/messages/tests/test_message.py |
489 | +++ b/lib/lp/services/messages/tests/test_message.py |
490 | @@ -13,21 +13,18 @@ from email.utils import ( |
491 | ) |
492 | |
493 | import six |
494 | -from testscenarios import WithScenarios |
495 | from testtools.matchers import ( |
496 | + ContainsDict, |
497 | + EndsWith, |
498 | Equals, |
499 | Is, |
500 | MatchesStructure, |
501 | ) |
502 | import transaction |
503 | from zope.security.interfaces import Unauthorized |
504 | -from zope.security.proxy import ( |
505 | - ProxyFactory, |
506 | - removeSecurityProxy, |
507 | - ) |
508 | +from zope.security.proxy import removeSecurityProxy |
509 | |
510 | from lp.bugs.interfaces.bugmessage import IBugMessage |
511 | -from lp.bugs.model.bugmessage import BugMessage |
512 | from lp.services.compat import message_as_bytes |
513 | from lp.services.database.interfaces import IStore |
514 | from lp.services.database.sqlbase import get_transaction_timestamp |
515 | @@ -35,12 +32,12 @@ from lp.services.messages.model.message import ( |
516 | MessageChunk, |
517 | MessageSet, |
518 | ) |
519 | +from lp.services.messages.tests.scenarios import MessageTypeScenariosMixin |
520 | from lp.services.webapp.interfaces import OAuthPermission |
521 | from lp.testing import ( |
522 | admin_logged_in, |
523 | api_url, |
524 | login, |
525 | - login_person, |
526 | person_logged_in, |
527 | TestCaseWithFactory, |
528 | ) |
529 | @@ -198,34 +195,6 @@ class TestMessageSet(TestCaseWithFactory): |
530 | self.assertEqual(self.high_characters.decode('latin-1'), result) |
531 | |
532 | |
533 | -class MessageTypeScenariosMixin(WithScenarios): |
534 | - |
535 | - scenarios = [ |
536 | - ("bug", {"message_type": "bug"}), |
537 | - ("question", {"message_type": "question"}), |
538 | - ("MP comment", {"message_type": "mp"}) |
539 | - ] |
540 | - |
541 | - def setUp(self): |
542 | - super(MessageTypeScenariosMixin, self).setUp() |
543 | - self.person = self.factory.makePerson() |
544 | - login_person(self.person) |
545 | - |
546 | - def makeMessage(self, content=None, **kwargs): |
547 | - owner = kwargs.pop('owner', self.person) |
548 | - if self.message_type == "bug": |
549 | - msg = self.factory.makeBugComment( |
550 | - owner=owner, body=content, **kwargs) |
551 | - return ProxyFactory(IStore(BugMessage).find( |
552 | - BugMessage, BugMessage.message == msg).one()) |
553 | - elif self.message_type == "question": |
554 | - question = self.factory.makeQuestion() |
555 | - return question.giveAnswer(owner, content) |
556 | - elif self.message_type == "mp": |
557 | - return self.factory.makeCodeReviewComment( |
558 | - sender=owner, body=content) |
559 | - |
560 | - |
561 | class TestMessageEditing(MessageTypeScenariosMixin, TestCaseWithFactory): |
562 | """Test editing scenarios for Message objects.""" |
563 | |
564 | @@ -366,6 +335,18 @@ class TestMessageEditingAPI(MessageTypeScenariosMixin, TestCaseWithFactory): |
565 | else: |
566 | return api_url(msg) |
567 | |
568 | + def test_api_get_basic_structure(self): |
569 | + msg = self.makeMessage(content="some content") |
570 | + ws = self.getWebservice(self.person) |
571 | + url = self.getMessageAPIURL(msg) |
572 | + obj = ws.get(url).jsonBody() |
573 | + self.assertThat(obj, ContainsDict(dict( |
574 | + revisions_collection_link=EndsWith("/revisions"), |
575 | + date_last_edited=Is(None), |
576 | + date_deleted=Is(None), |
577 | + content=Equals("some content"), |
578 | + ))) |
579 | + |
580 | def test_edit_message(self): |
581 | msg = self.makeMessage(content="initial content") |
582 | ws = self.getWebservice(self.person) |
583 | diff --git a/lib/lp/services/messages/tests/test_messagerevision.py b/lib/lp/services/messages/tests/test_messagerevision.py |
584 | index 3b411c9..80d1a14 100644 |
585 | --- a/lib/lp/services/messages/tests/test_messagerevision.py |
586 | +++ b/lib/lp/services/messages/tests/test_messagerevision.py |
587 | @@ -3,18 +3,30 @@ |
588 | |
589 | __metaclass__ = type |
590 | |
591 | -from lp.services.database.interfaces import IStore |
592 | -from lp.services.database.sqlbase import get_transaction_timestamp |
593 | +from testtools.matchers import ( |
594 | + ContainsDict, |
595 | + EndsWith, |
596 | + Equals, |
597 | + Is, |
598 | + MatchesListwise, |
599 | + Not, |
600 | + ) |
601 | from zope.security.interfaces import Unauthorized |
602 | from zope.security.proxy import ProxyFactory |
603 | |
604 | +from lp.bugs.interfaces.bugmessage import IBugMessage |
605 | +from lp.services.database.interfaces import IStore |
606 | +from lp.services.database.sqlbase import get_transaction_timestamp |
607 | +from lp.services.messages.tests.scenarios import MessageTypeScenariosMixin |
608 | +from lp.services.webapp.interfaces import OAuthPermission |
609 | from lp.testing import ( |
610 | + admin_logged_in, |
611 | + api_url, |
612 | person_logged_in, |
613 | TestCaseWithFactory, |
614 | ) |
615 | -from lp.testing.layers import ( |
616 | - DatabaseFunctionalLayer, |
617 | - ) |
618 | +from lp.testing.layers import DatabaseFunctionalLayer |
619 | +from lp.testing.pages import webservice_for_person |
620 | |
621 | |
622 | class TestMessageRevision(TestCaseWithFactory): |
623 | @@ -48,3 +60,106 @@ class TestMessageRevision(TestCaseWithFactory): |
624 | self.assertEqual(0, len(rev.chunks)) |
625 | self.assertEqual( |
626 | get_transaction_timestamp(IStore(rev)), rev.date_deleted) |
627 | + |
628 | + |
629 | +class TestMessageRevisionAPI(MessageTypeScenariosMixin, TestCaseWithFactory): |
630 | + """Test editing scenarios for message revisions API.""" |
631 | + |
632 | + layer = DatabaseFunctionalLayer |
633 | + |
634 | + def getWebservice(self, person): |
635 | + return webservice_for_person( |
636 | + person, permission=OAuthPermission.WRITE_PUBLIC, |
637 | + default_api_version="devel") |
638 | + |
639 | + def getMessageAPIURL(self, msg): |
640 | + with admin_logged_in(): |
641 | + if IBugMessage.providedBy(msg): |
642 | + # BugMessage has a special URL mapping that uses the |
643 | + # IMessage object itself. |
644 | + return api_url(msg.message) |
645 | + else: |
646 | + return api_url(msg) |
647 | + |
648 | + def test_get_message_revision_list(self): |
649 | + msg = self.makeMessage(content="initial content") |
650 | + msg.editContent("new content 1") |
651 | + msg.editContent("final content") |
652 | + ws = self.getWebservice(self.person) |
653 | + url = self.getMessageAPIURL(msg) |
654 | + ws_message = ws.get(url).jsonBody() |
655 | + |
656 | + revisions = ws.get(ws_message['revisions_collection_link']).jsonBody() |
657 | + self.assertThat(revisions, ContainsDict({ |
658 | + "start": Equals(0), |
659 | + "total_size": Equals(2)})) |
660 | + self.assertThat(revisions["entries"], MatchesListwise([ |
661 | + ContainsDict({ |
662 | + "date_created": Not(Is(None)), |
663 | + "date_deleted": Is(None), |
664 | + "content": Equals("initial content"), |
665 | + "self_link": EndsWith("/revisions/1") |
666 | + }), |
667 | + ContainsDict({ |
668 | + "date_created": Not(Is(None)), |
669 | + "date_deleted": Is(None), |
670 | + "content": Equals("new content 1"), |
671 | + "self_link": EndsWith("/revisions/2") |
672 | + })])) |
673 | + |
674 | + def test_get_single_revision(self): |
675 | + msg = self.makeMessage(content="initial content") |
676 | + msg.editContent("new content 1") |
677 | + ws = self.getWebservice(self.person) |
678 | + |
679 | + with person_logged_in(self.person): |
680 | + revision_url = api_url(msg.revisions[0]) |
681 | + revision = ws.get(revision_url).jsonBody() |
682 | + self.assertThat(revision, ContainsDict({ |
683 | + "date_created": Not(Is(None)), |
684 | + "date_deleted": Is(None), |
685 | + "content": Equals("initial content"), |
686 | + "self_link": EndsWith("/revisions/1") |
687 | + })) |
688 | + |
689 | + def test_delete_revision_content(self): |
690 | + msg = self.makeMessage(content="initial content") |
691 | + msg.editContent("new content 1") |
692 | + msg.editContent("final content") |
693 | + |
694 | + with person_logged_in(self.person): |
695 | + revision_url = api_url(msg.revisions[0]) |
696 | + |
697 | + ws = self.getWebservice(self.person) |
698 | + response = ws.named_post(revision_url, "deleteContent") |
699 | + self.assertEqual(200, response.status) |
700 | + |
701 | + revision = ws.get(revision_url).jsonBody() |
702 | + self.assertThat(revision, ContainsDict({ |
703 | + "date_created": Not(Is(None)), |
704 | + "date_deleted": Not(Is(None)), |
705 | + "content": Equals(""), |
706 | + "self_link": EndsWith("/revisions/1") |
707 | + })) |
708 | + |
709 | + def test_delete_revision_content_denied_for_non_owners(self): |
710 | + msg = self.makeMessage(content="initial content") |
711 | + msg.editContent("new content 1") |
712 | + msg.editContent("final content") |
713 | + someone_else = self.factory.makePerson() |
714 | + |
715 | + with person_logged_in(self.person): |
716 | + revision_url = api_url(msg.revisions[0]) |
717 | + |
718 | + ws = self.getWebservice(someone_else) |
719 | + response = ws.named_post(revision_url, "deleteContent") |
720 | + self.assertEqual(401, response.status) |
721 | + |
722 | + revision = ws.get(revision_url).jsonBody() |
723 | + self.assertThat(revision, ContainsDict({ |
724 | + "date_created": Not(Is(None)), |
725 | + "date_deleted": Is(None), |
726 | + "content": Equals("initial content"), |
727 | + "self_link": EndsWith("/revisions/1") |
728 | + })) |
729 | + |
730 | diff --git a/lib/lp/services/webservice/wadl-to-refhtml.xsl b/lib/lp/services/webservice/wadl-to-refhtml.xsl |
731 | index ee5e2e6..d0fbf23 100644 |
732 | --- a/lib/lp/services/webservice/wadl-to-refhtml.xsl |
733 | +++ b/lib/lp/services/webservice/wadl-to-refhtml.xsl |
734 | @@ -406,6 +406,12 @@ |
735 | <xsl:text>/comments/</xsl:text> |
736 | <var><index></var> |
737 | </xsl:when> |
738 | + <xsl:when test="@id = 'message_revision'"> |
739 | + <xsl:text>/</xsl:text> |
740 | + <var><message-url></var> |
741 | + <xsl:text>/revisions/</xsl:text> |
742 | + <var><index></var> |
743 | + </xsl:when> |
744 | <xsl:when test="@id = 'milestone'"> |
745 | <xsl:text>/</xsl:text> |
746 | <var><target.name></var> |
Merging failed /jenkins. ols.canonical. com/online- services/ job/launchpad/ 1659/
https:/