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