Merge ~pappacena/launchpad:comment-editing-revisions-api into launchpad: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)
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

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
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
index a61d39d..4b1ecff 100644
--- a/lib/lp/_schema_circular_imports.py
+++ b/lib/lp/_schema_circular_imports.py
@@ -149,6 +149,7 @@ from lp.services.messages.interfaces.message import (
149 IMessage,149 IMessage,
150 IUserToUserEmail,150 IUserToUserEmail,
151 )151 )
152from lp.services.messages.interfaces.messagerevision import IMessageRevision
152from lp.services.webservice.apihelpers import (153from lp.services.webservice.apihelpers import (
153 patch_collection_property,154 patch_collection_property,
154 patch_collection_return_type,155 patch_collection_return_type,
@@ -612,6 +613,7 @@ patch_reference_property(IIndexedMessage, 'inside', IBugTask)
612613
613# IMessage614# IMessage
614patch_reference_property(IMessage, 'owner', IPerson)615patch_reference_property(IMessage, 'owner', IPerson)
616patch_collection_property(IMessage, 'revisions', IMessageRevision)
615617
616# IUserToUserEmail618# IUserToUserEmail
617patch_reference_property(IUserToUserEmail, 'sender', IPerson)619patch_reference_property(IUserToUserEmail, 'sender', IPerson)
diff --git a/lib/lp/answers/browser/configure.zcml b/lib/lp/answers/browser/configure.zcml
index 631e8b8..87a6b76 100644
--- a/lib/lp/answers/browser/configure.zcml
+++ b/lib/lp/answers/browser/configure.zcml
@@ -282,6 +282,10 @@
282 module=".question"282 module=".question"
283 classes="QuestionNavigation"283 classes="QuestionNavigation"
284 />284 />
285 <browser:navigation
286 module=".question"
287 classes="QuestionMessageNavigation"
288 />
285289
286 <browser:url290 <browser:url
287 for="lp.answers.interfaces.questioncollection.IQuestionSet"291 for="lp.answers.interfaces.questioncollection.IQuestionSet"
diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py
index 814f329..68c4b59 100644
--- a/lib/lp/answers/browser/question.py
+++ b/lib/lp/answers/browser/question.py
@@ -71,6 +71,7 @@ from lp.answers.interfaces.question import (
71 IQuestionLinkFAQForm,71 IQuestionLinkFAQForm,
72 )72 )
73from lp.answers.interfaces.questioncollection import IQuestionSet73from lp.answers.interfaces.questioncollection import IQuestionSet
74from lp.answers.interfaces.questionmessage import IQuestionMessage
74from lp.answers.interfaces.questiontarget import (75from lp.answers.interfaces.questiontarget import (
75 IAnswersFrontPageSearchForm,76 IAnswersFrontPageSearchForm,
76 IQuestionTarget,77 IQuestionTarget,
@@ -275,6 +276,20 @@ class QuestionNavigation(Navigation):
275 return None276 return None
276277
277278
279class QuestionMessageNavigation(Navigation):
280 """Navigation for the IQuestionMessage."""
281
282 usedfor = IQuestionMessage
283
284 @stepthrough('revisions')
285 def traverse_revisions(self, revision):
286 try:
287 revision = int(revision)
288 except ValueError:
289 return None
290 return self.context.getRevisionByNumber(revision)
291
292
278class QuestionBreadcrumb(Breadcrumb):293class QuestionBreadcrumb(Breadcrumb):
279 """Builds a breadcrumb for an `IQuestion`."""294 """Builds a breadcrumb for an `IQuestion`."""
280295
diff --git a/lib/lp/answers/stories/webservice.txt b/lib/lp/answers/stories/webservice.txt
index 8ab277c..0954ced 100644
--- a/lib/lp/answers/stories/webservice.txt
+++ b/lib/lp/answers/stories/webservice.txt
@@ -242,6 +242,7 @@ that indicate how the message changed the question.
242 parent_link: None242 parent_link: None
243 question_link: 'http://api.launchpad.test/devel/my-project/+question/...'243 question_link: 'http://api.launchpad.test/devel/my-project/+question/...'
244 resource_type_link: 'http://api.launchpad.test/devel/#question_message'244 resource_type_link: 'http://api.launchpad.test/devel/#question_message'
245 revisions_collection_link: 'http://...'
245 self_link:246 self_link:
246 'http://api.launchpad.test/devel/my-project/+question/.../messages/1'247 'http://api.launchpad.test/devel/my-project/+question/.../messages/1'
247 subject: 'Re: Q 1 great'248 subject: 'Re: Q 1 great'
diff --git a/lib/lp/bugs/browser/bugcomment.py b/lib/lp/bugs/browser/bugcomment.py
index e40967f..6cc89f2 100644
--- a/lib/lp/bugs/browser/bugcomment.py
+++ b/lib/lp/bugs/browser/bugcomment.py
@@ -49,6 +49,8 @@ from lp.services.propertycache import (
49from lp.services.webapp import (49from lp.services.webapp import (
50 canonical_url,50 canonical_url,
51 LaunchpadView,51 LaunchpadView,
52 Navigation,
53 stepthrough,
52 )54 )
53from lp.services.webapp.breadcrumb import Breadcrumb55from lp.services.webapp.breadcrumb import Breadcrumb
54from lp.services.webapp.interfaces import ILaunchBag56from lp.services.webapp.interfaces import ILaunchBag
@@ -57,6 +59,19 @@ from lp.services.webapp.interfaces import ILaunchBag
57COMMENT_ACTIVITY_GROUPING_WINDOW = timedelta(minutes=5)59COMMENT_ACTIVITY_GROUPING_WINDOW = timedelta(minutes=5)
5860
5961
62class BugCommentNavigation(Navigation):
63 """Navigation for the `IBugComment`."""
64 usedfor = IBugComment
65
66 @stepthrough('revisions')
67 def traverse_revisions(self, revision):
68 try:
69 revision = int(revision)
70 except ValueError:
71 return None
72 return self.context.getRevisionByNumber(revision)
73
74
60def build_comments_from_chunks(75def build_comments_from_chunks(
61 bugtask, truncate=False, slice_info=None, show_spam_controls=False,76 bugtask, truncate=False, slice_info=None, show_spam_controls=False,
62 user=None, hide_first=False):77 user=None, hide_first=False):
diff --git a/lib/lp/bugs/browser/configure.zcml b/lib/lp/bugs/browser/configure.zcml
index ac80819..d9c36a8 100644
--- a/lib/lp/bugs/browser/configure.zcml
+++ b/lib/lp/bugs/browser/configure.zcml
@@ -1,4 +1,4 @@
1<!-- Copyright 2010-2014 Canonical Ltd. This software is licensed under the1<!-- Copyright 2010-2021 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -163,6 +163,10 @@
163 path_expression="string:comments/${index}"163 path_expression="string:comments/${index}"
164 attribute_to_parent="bugtask"164 attribute_to_parent="bugtask"
165 rootsite="bugs"/>165 rootsite="bugs"/>
166 <browser:navigation
167 module=".bugcomment"
168 classes="BugCommentNavigation"
169 />
166 <browser:page170 <browser:page
167 for="lp.bugs.interfaces.bugmessage.IBugComment"171 for="lp.bugs.interfaces.bugmessage.IBugComment"
168 name="+index"172 name="+index"
diff --git a/lib/lp/code/browser/codereviewcomment.py b/lib/lp/code/browser/codereviewcomment.py
index 0d0496d..094cd79 100644
--- a/lib/lp/code/browser/codereviewcomment.py
+++ b/lib/lp/code/browser/codereviewcomment.py
@@ -56,10 +56,25 @@ from lp.services.webapp import (
56 ContextMenu,56 ContextMenu,
57 LaunchpadView,57 LaunchpadView,
58 Link,58 Link,
59 Navigation,
60 stepthrough,
59 )61 )
60from lp.services.webapp.interfaces import ILaunchBag62from lp.services.webapp.interfaces import ILaunchBag
6163
6264
65class CodeReviewCommentNavigation(Navigation):
66 """Navigation for the `ICodeReviewComment`."""
67 usedfor = ICodeReviewComment
68
69 @stepthrough('revisions')
70 def traverse_revisions(self, revision):
71 try:
72 revision = int(revision)
73 except ValueError:
74 return None
75 return self.context.getRevisionByNumber(int(revision))
76
77
63class ICodeReviewDisplayComment(IComment, ICodeReviewComment):78class ICodeReviewDisplayComment(IComment, ICodeReviewComment):
64 """Marker interface for displaying code review comments."""79 """Marker interface for displaying code review comments."""
65 message = Object(schema=IMessage, title=_('The message.'))80 message = Object(schema=IMessage, title=_('The message.'))
diff --git a/lib/lp/code/browser/configure.zcml b/lib/lp/code/browser/configure.zcml
index e7943c0..f7bab39 100644
--- a/lib/lp/code/browser/configure.zcml
+++ b/lib/lp/code/browser/configure.zcml
@@ -1,4 +1,4 @@
1<!-- Copyright 2009-2020 Canonical Ltd. This software is licensed under the1<!-- Copyright 2009-2021 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -556,6 +556,9 @@
556 path_expression="string:comments/${id}"556 path_expression="string:comments/${id}"
557 attribute_to_parent="branch_merge_proposal"557 attribute_to_parent="branch_merge_proposal"
558 rootsite="code"/>558 rootsite="code"/>
559 <browser:navigation
560 module=".codereviewcomment"
561 classes="CodeReviewCommentNavigation" />
559 <browser:defaultView562 <browser:defaultView
560 for="lp.code.interfaces.codereviewcomment.ICodeReviewComment"563 for="lp.code.interfaces.codereviewcomment.ICodeReviewComment"
561 name="+index"/>564 name="+index"/>
diff --git a/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt b/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
index 58b86e7..ccb1c9d 100644
--- a/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
+++ b/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
@@ -206,6 +206,7 @@ The comments on a branch merge proposal are exposed through the API.
206 message_body: 'This is great work'206 message_body: 'This is great work'
207 owner_link: 'http://...'207 owner_link: 'http://...'
208 resource_type_link: 'http://.../#code_review_comment'208 resource_type_link: 'http://.../#code_review_comment'
209 revisions_collection_link: 'http://...'
209 self_link: 'http://.../~source/fooix/fix-it/+merge/.../comments/...'210 self_link: 'http://.../~source/fooix/fix-it/+merge/.../comments/...'
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'
211 vote: 'Approve'212 vote: 'Approve'
@@ -228,6 +229,7 @@ The comments on a branch merge proposal are exposed through the API.
228 message_body: 'This is mediocre work.'229 message_body: 'This is mediocre work.'
229 owner_link: 'http://...'230 owner_link: 'http://...'
230 resource_type_link: 'http://.../#code_review_comment'231 resource_type_link: 'http://.../#code_review_comment'
232 revisions_collection_link: 'http://...'
231 self_link: 'http://.../~source/fooix/fix-it/+merge/.../comments/...'233 self_link: 'http://.../~source/fooix/fix-it/+merge/.../comments/...'
232 title: ...234 title: ...
233 vote: 'Abstain'235 vote: 'Abstain'
@@ -306,6 +308,7 @@ Now the code review should be made.
306 message_body: 'This is great work'308 message_body: 'This is great work'
307 owner_link: 'http://...'309 owner_link: 'http://...'
308 resource_type_link: 'http://.../#code_review_comment'310 resource_type_link: 'http://.../#code_review_comment'
311 revisions_collection_link: 'http://...'
309 self_link: 'http://.../~source/fooix/fix-it/+merge/.../comments/...'312 self_link: 'http://.../~source/fooix/fix-it/+merge/.../comments/...'
310 title: ...313 title: ...
311 vote: 'Approve'314 vote: 'Approve'
diff --git a/lib/lp/services/messages/browser/configure.zcml b/lib/lp/services/messages/browser/configure.zcml
312new file mode 100644315new file mode 100644
index 0000000..0413ba9
--- /dev/null
+++ b/lib/lp/services/messages/browser/configure.zcml
@@ -0,0 +1,10 @@
1<configure
2 xmlns="http://namespaces.zope.org/zope"
3 xmlns:browser="http://namespaces.zope.org/browser"
4 xmlns:i18n="http://namespaces.zope.org/i18n"
5 i18n_domain="launchpad">
6 <browser:url
7 for="lp.services.messages.interfaces.messagerevision.IMessageRevision"
8 path_expression="string:revisions/${revision}"
9 attribute_to_parent="message_implementation" />
10</configure>
diff --git a/lib/lp/services/messages/browser/message.py b/lib/lp/services/messages/browser/message.py
index 06800e6..1e4a6e7 100644
--- a/lib/lp/services/messages/browser/message.py
+++ b/lib/lp/services/messages/browser/message.py
@@ -7,6 +7,7 @@ __metaclass__ = type
77
8from zope.interface import implementer8from zope.interface import implementer
99
10from lp.bugs.interfaces.bugmessage import IBugMessage
10from lp.services.messages.interfaces.message import IIndexedMessage11from lp.services.messages.interfaces.message import IIndexedMessage
11from lp.services.webapp.interfaces import ICanonicalUrlData12from lp.services.webapp.interfaces import ICanonicalUrlData
1213
@@ -28,6 +29,9 @@ class BugMessageCanonicalUrlData:
2829
29 def __init__(self, bug, message):30 def __init__(self, bug, message):
30 self.inside = bug.default_bugtask31 self.inside = bug.default_bugtask
32 if IBugMessage.providedBy(message):
33 # bug.messages is a list of Message objects, not BugMessage.
34 message = message.message
31 self.path = "comments/%d" % list(bug.messages).index(message)35 self.path = "comments/%d" % list(bug.messages).index(message)
3236
3337
diff --git a/lib/lp/services/messages/configure.zcml b/lib/lp/services/messages/configure.zcml
index 19cb3c3..867b260 100644
--- a/lib/lp/services/messages/configure.zcml
+++ b/lib/lp/services/messages/configure.zcml
@@ -82,4 +82,5 @@
82 />82 />
8383
84 <webservice:register module="lp.services.messages.interfaces.webservice" />84 <webservice:register module="lp.services.messages.interfaces.webservice" />
85 <include package=".browser"/>
85</configure>86</configure>
diff --git a/lib/lp/services/messages/interfaces/message.py b/lib/lp/services/messages/interfaces/message.py
index 6d4e186..598b665 100644
--- a/lib/lp/services/messages/interfaces/message.py
+++ b/lib/lp/services/messages/interfaces/message.py
@@ -88,7 +88,14 @@ class IMessageCommon(Interface):
88 Reference(title=_('Person'), schema=Interface,88 Reference(title=_('Person'), schema=Interface,
89 required=False, readonly=True))89 required=False, readonly=True))
9090
91 revisions = Attribute(_('Message revision history'))91 revisions = exported(CollectionField(
92 title=_("Message revision history"),
93 description=_(
94 "Revision history of this message, sorted in ascending order."),
95 # Really IMessageRevision, patched in _schema_circular_imports.
96 value_type=Reference(schema=Interface),
97 required=False, readonly=True), as_of="devel")
98
92 datecreated = exported(99 datecreated = exported(
93 Datetime(title=_('Date Created'), required=True, readonly=True),100 Datetime(title=_('Date Created'), required=True, readonly=True),
94 exported_as='date_created')101 exported_as='date_created')
@@ -100,6 +107,9 @@ class IMessageCommon(Interface):
100 title=_('When this message was deleted'), required=False,107 title=_('When this message was deleted'), required=False,
101 readonly=True))108 readonly=True))
102109
110 def getRevisionByNumber(revision_number):
111 """Returns the revision with the given number."""
112
103113
104class IMessageView(IMessageCommon):114class IMessageView(IMessageCommon):
105 """Public attributes for message.115 """Public attributes for message.
diff --git a/lib/lp/services/messages/interfaces/messagerevision.py b/lib/lp/services/messages/interfaces/messagerevision.py
index 49d3b9f..3ee5a90 100644
--- a/lib/lp/services/messages/interfaces/messagerevision.py
+++ b/lib/lp/services/messages/interfaces/messagerevision.py
@@ -10,6 +10,12 @@ __all__ = [
10 'IMessageRevisionChunk',10 'IMessageRevisionChunk',
11 ]11 ]
1212
13from lazr.restful.declarations import (
14 export_write_operation,
15 exported,
16 exported_as_webservice_entry,
17 operation_for_version,
18 )
13from lazr.restful.fields import Reference19from lazr.restful.fields import Reference
14from zope.interface import (20from zope.interface import (
15 Attribute,21 Attribute,
@@ -31,21 +37,26 @@ class IMessageRevisionView(Interface):
3137
32 revision = Int(title=_("Revision number"), required=True, readonly=True)38 revision = Int(title=_("Revision number"), required=True, readonly=True)
3339
34 content = Text(40 content = exported(Text(
35 title=_("The message at the given revision"),41 title=_("The message at the given revision"),
36 required=True, readonly=True)42 required=True, readonly=True))
3743
38 message = Reference(44 message = Reference(
39 title=_('The current message of this revision.'),45 title=_('The current message of this revision.'),
40 schema=IMessage, required=True, readonly=True)46 schema=IMessage, required=True, readonly=True)
4147
42 date_created = Datetime(48 message_implementation = Reference(
49 title=_('The message implementation (BugComment, QuestionMessage or '
50 'CodeReviewComment) related to this revision'),
51 schema=IMessage, required=True, readonly=True)
52
53 date_created = exported(Datetime(
43 title=_("The time when this message revision was created."),54 title=_("The time when this message revision was created."),
44 required=True, readonly=True)55 required=True, readonly=True))
4556
46 date_deleted = Datetime(57 date_deleted = exported(Datetime(
47 title=_("The time when this message revision was created."),58 title=_("The time when this message revision was created."),
48 required=False, readonly=True)59 required=False, readonly=True))
4960
50 chunks = Attribute(_('Message pieces'))61 chunks = Attribute(_('Message pieces'))
5162
@@ -53,10 +64,13 @@ class IMessageRevisionView(Interface):
53class IMessageRevisionEdit(Interface):64class IMessageRevisionEdit(Interface):
54 """IMessageRevision editable attributes."""65 """IMessageRevision editable attributes."""
5566
67 @export_write_operation()
68 @operation_for_version("devel")
56 def deleteContent():69 def deleteContent():
57 """Deletes this MessageRevision content."""70 """Deletes this MessageRevision content."""
5871
5972
73@exported_as_webservice_entry(publish_web_link=False, as_of="devel")
60class IMessageRevision(IMessageRevisionView, IMessageRevisionEdit):74class IMessageRevision(IMessageRevisionView, IMessageRevisionEdit):
61 """A historical revision of a IMessage."""75 """A historical revision of a IMessage."""
6276
diff --git a/lib/lp/services/messages/interfaces/webservice.py b/lib/lp/services/messages/interfaces/webservice.py
index 94960a9..1dade00 100644
--- a/lib/lp/services/messages/interfaces/webservice.py
+++ b/lib/lp/services/messages/interfaces/webservice.py
@@ -1,4 +1,4 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the1# Copyright 2011-2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""All the interfaces that are exposed through the webservice.4"""All the interfaces that are exposed through the webservice.
@@ -12,10 +12,12 @@ which tells `lazr.restful` that it should look for webservice exports here.
12__metaclass__ = type12__metaclass__ = type
13__all__ = [13__all__ = [
14 'IMessage',14 'IMessage',
15 'IMessageRevision',
15 ]16 ]
1617
17from lp import _schema_circular_imports18from lp import _schema_circular_imports
18from lp.services.messages.interfaces.message import IMessage19from lp.services.messages.interfaces.message import IMessage
20from lp.services.messages.interfaces.messagerevision import IMessageRevision
1921
2022
21_schema_circular_imports23_schema_circular_imports
diff --git a/lib/lp/services/messages/model/message.py b/lib/lp/services/messages/model/message.py
index d20ca2f..f6638b2 100644
--- a/lib/lp/services/messages/model/message.py
+++ b/lib/lp/services/messages/model/message.py
@@ -174,13 +174,20 @@ class Message(SQLBase):
174 """See `IMessage`."""174 """See `IMessage`."""
175 return None175 return None
176176
177 @property
178 def _revisions(self):
179 return Store.of(self).find(
180 MessageRevision,
181 MessageRevision.message == self
182 ).order_by(MessageRevision.revision)
183
177 @cachedproperty184 @cachedproperty
178 def revisions(self):185 def revisions(self):
179 """See `IMessage`."""186 """See `IMessage`."""
180 return list(Store.of(self).find(187 return list(self._revisions)
181 MessageRevision,188
182 MessageRevision.message == self189 def getRevisionByNumber(self, revision_number):
183 ).order_by(MessageRevision.revision))190 return self._revisions.find(revision=revision_number).one()
184191
185 def editContent(self, new_content):192 def editContent(self, new_content):
186 """See `IMessage`."""193 """See `IMessage`."""
diff --git a/lib/lp/services/messages/model/messagerevision.py b/lib/lp/services/messages/model/messagerevision.py
index f4a1466..535ff66 100644
--- a/lib/lp/services/messages/model/messagerevision.py
+++ b/lib/lp/services/messages/model/messagerevision.py
@@ -57,6 +57,27 @@ class MessageRevision(StormBase):
57 self.date_created = date_created57 self.date_created = date_created
58 self.date_deleted = date_deleted58 self.date_deleted = date_deleted
5959
60 @property
61 def message_implementation(self):
62 from lp.bugs.model.bugmessage import BugMessage
63 from lp.code.model.codereviewcomment import CodeReviewComment
64 from lp.answers.model.questionmessage import QuestionMessage
65
66 store = IStore(self)
67 (identifier, ) = store.execute("""
68 SELECT 'bug' FROM BugMessage WHERE message = %s
69 UNION
70 SELECT 'question' FROM QuestionMessage WHERE message = %s
71 UNION
72 SELECT 'mp' FROM CodeReviewMessage WHERE message = %s;
73 """, params=[self.message_id] * 3).get_one()
74 id_to_class = {
75 "bug": BugMessage,
76 "question": QuestionMessage,
77 "mp": CodeReviewComment}
78 klass = id_to_class[identifier]
79 return store.find(klass, klass.message == self.message_id).one()
80
60 @cachedproperty81 @cachedproperty
61 def chunks(self):82 def chunks(self):
62 return list(IStore(self).find(83 return list(IStore(self).find(
diff --git a/lib/lp/services/messages/tests/scenarios.py b/lib/lp/services/messages/tests/scenarios.py
63new file mode 10064484new file mode 100644
index 0000000..f366574
--- /dev/null
+++ b/lib/lp/services/messages/tests/scenarios.py
@@ -0,0 +1,41 @@
1# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6from testscenarios import WithScenarios
7from zope.security.proxy import ProxyFactory
8
9from lp.bugs.model.bugmessage import BugMessage
10from lp.services.database.interfaces import IStore
11from lp.testing import (
12 login_person,
13 )
14
15
16class MessageTypeScenariosMixin(WithScenarios):
17
18 scenarios = [
19 ("bug", {"message_type": "bug"}),
20 ("question", {"message_type": "question"}),
21 ("MP comment", {"message_type": "mp"})
22 ]
23
24 def setUp(self):
25 super(MessageTypeScenariosMixin, self).setUp()
26 self.person = self.factory.makePerson()
27 login_person(self.person)
28
29 def makeMessage(self, content=None, **kwargs):
30 owner = kwargs.pop('owner', self.person)
31 if self.message_type == "bug":
32 msg = self.factory.makeBugComment(
33 owner=owner, body=content, **kwargs)
34 return ProxyFactory(IStore(BugMessage).find(
35 BugMessage, BugMessage.message == msg).one())
36 elif self.message_type == "question":
37 question = self.factory.makeQuestion()
38 return question.giveAnswer(owner, content)
39 elif self.message_type == "mp":
40 return self.factory.makeCodeReviewComment(
41 sender=owner, body=content)
diff --git a/lib/lp/services/messages/tests/test_message.py b/lib/lp/services/messages/tests/test_message.py
index 070bc91..c483305 100644
--- a/lib/lp/services/messages/tests/test_message.py
+++ b/lib/lp/services/messages/tests/test_message.py
@@ -13,21 +13,18 @@ from email.utils import (
13 )13 )
1414
15import six15import six
16from testscenarios import WithScenarios
17from testtools.matchers import (16from testtools.matchers import (
17 ContainsDict,
18 EndsWith,
18 Equals,19 Equals,
19 Is,20 Is,
20 MatchesStructure,21 MatchesStructure,
21 )22 )
22import transaction23import transaction
23from zope.security.interfaces import Unauthorized24from zope.security.interfaces import Unauthorized
24from zope.security.proxy import (25from zope.security.proxy import removeSecurityProxy
25 ProxyFactory,
26 removeSecurityProxy,
27 )
2826
29from lp.bugs.interfaces.bugmessage import IBugMessage27from lp.bugs.interfaces.bugmessage import IBugMessage
30from lp.bugs.model.bugmessage import BugMessage
31from lp.services.compat import message_as_bytes28from lp.services.compat import message_as_bytes
32from lp.services.database.interfaces import IStore29from lp.services.database.interfaces import IStore
33from lp.services.database.sqlbase import get_transaction_timestamp30from lp.services.database.sqlbase import get_transaction_timestamp
@@ -35,12 +32,12 @@ from lp.services.messages.model.message import (
35 MessageChunk,32 MessageChunk,
36 MessageSet,33 MessageSet,
37 )34 )
35from lp.services.messages.tests.scenarios import MessageTypeScenariosMixin
38from lp.services.webapp.interfaces import OAuthPermission36from lp.services.webapp.interfaces import OAuthPermission
39from lp.testing import (37from lp.testing import (
40 admin_logged_in,38 admin_logged_in,
41 api_url,39 api_url,
42 login,40 login,
43 login_person,
44 person_logged_in,41 person_logged_in,
45 TestCaseWithFactory,42 TestCaseWithFactory,
46 )43 )
@@ -198,34 +195,6 @@ class TestMessageSet(TestCaseWithFactory):
198 self.assertEqual(self.high_characters.decode('latin-1'), result)195 self.assertEqual(self.high_characters.decode('latin-1'), result)
199196
200197
201class MessageTypeScenariosMixin(WithScenarios):
202
203 scenarios = [
204 ("bug", {"message_type": "bug"}),
205 ("question", {"message_type": "question"}),
206 ("MP comment", {"message_type": "mp"})
207 ]
208
209 def setUp(self):
210 super(MessageTypeScenariosMixin, self).setUp()
211 self.person = self.factory.makePerson()
212 login_person(self.person)
213
214 def makeMessage(self, content=None, **kwargs):
215 owner = kwargs.pop('owner', self.person)
216 if self.message_type == "bug":
217 msg = self.factory.makeBugComment(
218 owner=owner, body=content, **kwargs)
219 return ProxyFactory(IStore(BugMessage).find(
220 BugMessage, BugMessage.message == msg).one())
221 elif self.message_type == "question":
222 question = self.factory.makeQuestion()
223 return question.giveAnswer(owner, content)
224 elif self.message_type == "mp":
225 return self.factory.makeCodeReviewComment(
226 sender=owner, body=content)
227
228
229class TestMessageEditing(MessageTypeScenariosMixin, TestCaseWithFactory):198class TestMessageEditing(MessageTypeScenariosMixin, TestCaseWithFactory):
230 """Test editing scenarios for Message objects."""199 """Test editing scenarios for Message objects."""
231200
@@ -366,6 +335,18 @@ class TestMessageEditingAPI(MessageTypeScenariosMixin, TestCaseWithFactory):
366 else:335 else:
367 return api_url(msg)336 return api_url(msg)
368337
338 def test_api_get_basic_structure(self):
339 msg = self.makeMessage(content="some content")
340 ws = self.getWebservice(self.person)
341 url = self.getMessageAPIURL(msg)
342 obj = ws.get(url).jsonBody()
343 self.assertThat(obj, ContainsDict(dict(
344 revisions_collection_link=EndsWith("/revisions"),
345 date_last_edited=Is(None),
346 date_deleted=Is(None),
347 content=Equals("some content"),
348 )))
349
369 def test_edit_message(self):350 def test_edit_message(self):
370 msg = self.makeMessage(content="initial content")351 msg = self.makeMessage(content="initial content")
371 ws = self.getWebservice(self.person)352 ws = self.getWebservice(self.person)
diff --git a/lib/lp/services/messages/tests/test_messagerevision.py b/lib/lp/services/messages/tests/test_messagerevision.py
index 3b411c9..80d1a14 100644
--- a/lib/lp/services/messages/tests/test_messagerevision.py
+++ b/lib/lp/services/messages/tests/test_messagerevision.py
@@ -3,18 +3,30 @@
33
4__metaclass__ = type4__metaclass__ = type
55
6from lp.services.database.interfaces import IStore6from testtools.matchers import (
7from lp.services.database.sqlbase import get_transaction_timestamp7 ContainsDict,
8 EndsWith,
9 Equals,
10 Is,
11 MatchesListwise,
12 Not,
13 )
8from zope.security.interfaces import Unauthorized14from zope.security.interfaces import Unauthorized
9from zope.security.proxy import ProxyFactory15from zope.security.proxy import ProxyFactory
1016
17from lp.bugs.interfaces.bugmessage import IBugMessage
18from lp.services.database.interfaces import IStore
19from lp.services.database.sqlbase import get_transaction_timestamp
20from lp.services.messages.tests.scenarios import MessageTypeScenariosMixin
21from lp.services.webapp.interfaces import OAuthPermission
11from lp.testing import (22from lp.testing import (
23 admin_logged_in,
24 api_url,
12 person_logged_in,25 person_logged_in,
13 TestCaseWithFactory,26 TestCaseWithFactory,
14 )27 )
15from lp.testing.layers import (28from lp.testing.layers import DatabaseFunctionalLayer
16 DatabaseFunctionalLayer,29from lp.testing.pages import webservice_for_person
17 )
1830
1931
20class TestMessageRevision(TestCaseWithFactory):32class TestMessageRevision(TestCaseWithFactory):
@@ -48,3 +60,106 @@ class TestMessageRevision(TestCaseWithFactory):
48 self.assertEqual(0, len(rev.chunks))60 self.assertEqual(0, len(rev.chunks))
49 self.assertEqual(61 self.assertEqual(
50 get_transaction_timestamp(IStore(rev)), rev.date_deleted)62 get_transaction_timestamp(IStore(rev)), rev.date_deleted)
63
64
65class TestMessageRevisionAPI(MessageTypeScenariosMixin, TestCaseWithFactory):
66 """Test editing scenarios for message revisions API."""
67
68 layer = DatabaseFunctionalLayer
69
70 def getWebservice(self, person):
71 return webservice_for_person(
72 person, permission=OAuthPermission.WRITE_PUBLIC,
73 default_api_version="devel")
74
75 def getMessageAPIURL(self, msg):
76 with admin_logged_in():
77 if IBugMessage.providedBy(msg):
78 # BugMessage has a special URL mapping that uses the
79 # IMessage object itself.
80 return api_url(msg.message)
81 else:
82 return api_url(msg)
83
84 def test_get_message_revision_list(self):
85 msg = self.makeMessage(content="initial content")
86 msg.editContent("new content 1")
87 msg.editContent("final content")
88 ws = self.getWebservice(self.person)
89 url = self.getMessageAPIURL(msg)
90 ws_message = ws.get(url).jsonBody()
91
92 revisions = ws.get(ws_message['revisions_collection_link']).jsonBody()
93 self.assertThat(revisions, ContainsDict({
94 "start": Equals(0),
95 "total_size": Equals(2)}))
96 self.assertThat(revisions["entries"], MatchesListwise([
97 ContainsDict({
98 "date_created": Not(Is(None)),
99 "date_deleted": Is(None),
100 "content": Equals("initial content"),
101 "self_link": EndsWith("/revisions/1")
102 }),
103 ContainsDict({
104 "date_created": Not(Is(None)),
105 "date_deleted": Is(None),
106 "content": Equals("new content 1"),
107 "self_link": EndsWith("/revisions/2")
108 })]))
109
110 def test_get_single_revision(self):
111 msg = self.makeMessage(content="initial content")
112 msg.editContent("new content 1")
113 ws = self.getWebservice(self.person)
114
115 with person_logged_in(self.person):
116 revision_url = api_url(msg.revisions[0])
117 revision = ws.get(revision_url).jsonBody()
118 self.assertThat(revision, ContainsDict({
119 "date_created": Not(Is(None)),
120 "date_deleted": Is(None),
121 "content": Equals("initial content"),
122 "self_link": EndsWith("/revisions/1")
123 }))
124
125 def test_delete_revision_content(self):
126 msg = self.makeMessage(content="initial content")
127 msg.editContent("new content 1")
128 msg.editContent("final content")
129
130 with person_logged_in(self.person):
131 revision_url = api_url(msg.revisions[0])
132
133 ws = self.getWebservice(self.person)
134 response = ws.named_post(revision_url, "deleteContent")
135 self.assertEqual(200, response.status)
136
137 revision = ws.get(revision_url).jsonBody()
138 self.assertThat(revision, ContainsDict({
139 "date_created": Not(Is(None)),
140 "date_deleted": Not(Is(None)),
141 "content": Equals(""),
142 "self_link": EndsWith("/revisions/1")
143 }))
144
145 def test_delete_revision_content_denied_for_non_owners(self):
146 msg = self.makeMessage(content="initial content")
147 msg.editContent("new content 1")
148 msg.editContent("final content")
149 someone_else = self.factory.makePerson()
150
151 with person_logged_in(self.person):
152 revision_url = api_url(msg.revisions[0])
153
154 ws = self.getWebservice(someone_else)
155 response = ws.named_post(revision_url, "deleteContent")
156 self.assertEqual(401, response.status)
157
158 revision = ws.get(revision_url).jsonBody()
159 self.assertThat(revision, ContainsDict({
160 "date_created": Not(Is(None)),
161 "date_deleted": Is(None),
162 "content": Equals("initial content"),
163 "self_link": EndsWith("/revisions/1")
164 }))
165
diff --git a/lib/lp/services/webservice/wadl-to-refhtml.xsl b/lib/lp/services/webservice/wadl-to-refhtml.xsl
index ee5e2e6..d0fbf23 100644
--- a/lib/lp/services/webservice/wadl-to-refhtml.xsl
+++ b/lib/lp/services/webservice/wadl-to-refhtml.xsl
@@ -406,6 +406,12 @@
406 <xsl:text>/comments/</xsl:text>406 <xsl:text>/comments/</xsl:text>
407 <var>&lt;index&gt;</var>407 <var>&lt;index&gt;</var>
408 </xsl:when>408 </xsl:when>
409 <xsl:when test="@id = 'message_revision'">
410 <xsl:text>/</xsl:text>
411 <var>&lt;message-url&gt;</var>
412 <xsl:text>/revisions/</xsl:text>
413 <var>&lt;index&gt;</var>
414 </xsl:when>
409 <xsl:when test="@id = 'milestone'">415 <xsl:when test="@id = 'milestone'">
410 <xsl:text>/</xsl:text>416 <xsl:text>/</xsl:text>
411 <var>&lt;target.name&gt;</var>417 <var>&lt;target.name&gt;</var>