launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #27007
[Merge] ~pappacena/launchpad:comment-editing-revisions-api into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:comment-editing-revisions-api into launchpad:master with ~pappacena/launchpad:comment-editing-api as a prerequisite.
Commit message:
API to get and delete comment's revision history for bug messages, answers and code review comments
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/402285
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:comment-editing-revisions-api into launchpad:master.
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 (
IMessage,
IUserToUserEmail,
)
+from lp.services.messages.interfaces.messagerevision import IMessageRevision
from lp.services.webservice.apihelpers import (
patch_collection_property,
patch_collection_return_type,
@@ -612,6 +613,7 @@ patch_reference_property(IIndexedMessage, 'inside', IBugTask)
# IMessage
patch_reference_property(IMessage, 'owner', IPerson)
+patch_collection_property(IMessage, 'revisions', IMessageRevision)
# IUserToUserEmail
patch_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 @@
module=".question"
classes="QuestionNavigation"
/>
+ <browser:navigation
+ module=".question"
+ classes="QuestionMessageNavigation"
+ />
<browser:url
for="lp.answers.interfaces.questioncollection.IQuestionSet"
diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py
index 757da13..18b4c47 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 (
IQuestionLinkFAQForm,
)
from lp.answers.interfaces.questioncollection import IQuestionSet
+from lp.answers.interfaces.questionmessage import IQuestionMessage
from lp.answers.interfaces.questiontarget import (
IAnswersFrontPageSearchForm,
IQuestionTarget,
@@ -269,6 +270,17 @@ class QuestionNavigation(Navigation):
return self.context.messages[index]
+class QuestionMessageNavigation(Navigation):
+ """Navigation for the IQuestionMessage."""
+
+ usedfor = IQuestionMessage
+
+ @stepthrough('revisions')
+ def traverse_comments(self, index):
+ index = int(index) - 1
+ return self.context.revisions[index]
+
+
class QuestionBreadcrumb(Breadcrumb):
"""Builds a breadcrumb for an `IQuestion`."""
diff --git a/lib/lp/answers/stories/webservice.txt b/lib/lp/answers/stories/webservice.txt
index eb01af6..9df228f 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.
parent_link: None
question_link: 'http://api.launchpad.test/devel/my-project/+question/...'
resource_type_link: 'http://api.launchpad.test/devel/#question_message'
+ revisions_collection_link: 'http://...'
self_link:
'http://api.launchpad.test/devel/my-project/+question/.../messages/1'
subject: 'Re: Q 1 great'
diff --git a/lib/lp/bugs/browser/bugcomment.py b/lib/lp/bugs/browser/bugcomment.py
index e40967f..2302d83 100644
--- a/lib/lp/bugs/browser/bugcomment.py
+++ b/lib/lp/bugs/browser/bugcomment.py
@@ -49,6 +49,8 @@ from lp.services.propertycache import (
from lp.services.webapp import (
canonical_url,
LaunchpadView,
+ Navigation,
+ stepthrough,
)
from lp.services.webapp.breadcrumb import Breadcrumb
from lp.services.webapp.interfaces import ILaunchBag
@@ -57,6 +59,16 @@ from lp.services.webapp.interfaces import ILaunchBag
COMMENT_ACTIVITY_GROUPING_WINDOW = timedelta(minutes=5)
+class BugCommentNavigation(Navigation):
+ """Navigation for the `IBugComment`."""
+ usedfor = IBugComment
+
+ @stepthrough('revisions')
+ def traverse_comments(self, index):
+ index = int(index) - 1
+ return self.context.revisions[index]
+
+
def build_comments_from_chunks(
bugtask, truncate=False, slice_info=None, show_spam_controls=False,
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 @@
-<!-- Copyright 2010-2014 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2010-2021 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -163,6 +163,10 @@
path_expression="string:comments/${index}"
attribute_to_parent="bugtask"
rootsite="bugs"/>
+ <browser:navigation
+ module=".bugcomment"
+ classes="BugCommentNavigation"
+ />
<browser:page
for="lp.bugs.interfaces.bugmessage.IBugComment"
name="+index"
diff --git a/lib/lp/bugs/stories/webservice/xx-bug.txt b/lib/lp/bugs/stories/webservice/xx-bug.txt
index 0c43c88..17fa5e7 100644
--- a/lib/lp/bugs/stories/webservice/xx-bug.txt
+++ b/lib/lp/bugs/stories/webservice/xx-bug.txt
@@ -239,6 +239,7 @@ Each bug has a collection of messages.
owner_link: 'http://.../~name12'
parent_link: None
resource_type_link: 'http://.../#message'
+ revisions_collection_link: '...'
self_link: 'http://.../firefox/+bug/5/comments/0'
subject: 'Firefox install instructions should be complete'
web_link: 'http://bugs.../firefox/+bug/5/comments/0'
@@ -284,6 +285,7 @@ We can add a new message to a bug by calling the newMessage method.
content: 'This is a new message added through the webservice API.'
...
resource_type_link: 'http://api.launchpad.test/beta/#message'
+ revisions_collection_link: '...'
self_link: 'http://api.launchpad.test/beta/firefox/+bug/5/comments/1'
subject: 'A new message'
web_link: '...'
@@ -1354,6 +1356,7 @@ attachment. This is where our comment is recorded.
owner_link: 'http://.../~salgado'
parent_link: None
resource_type_link: 'http://.../#message'
+ revisions_collection_link: '...'
self_link: 'http://.../firefox/+bug/1/comments/2'
subject: 'Re: Firefox does not support SVG'
web_link: 'http://bugs.../firefox/+bug/1/comments/2'
diff --git a/lib/lp/code/browser/codereviewcomment.py b/lib/lp/code/browser/codereviewcomment.py
index 0d0496d..c4ca667 100644
--- a/lib/lp/code/browser/codereviewcomment.py
+++ b/lib/lp/code/browser/codereviewcomment.py
@@ -56,10 +56,22 @@ from lp.services.webapp import (
ContextMenu,
LaunchpadView,
Link,
+ Navigation,
+ stepthrough,
)
from lp.services.webapp.interfaces import ILaunchBag
+class CodeReviewCommentNavigation(Navigation):
+ """Navigation for the `ICodeReviewComment`."""
+ usedfor = ICodeReviewComment
+
+ @stepthrough('revisions')
+ def traverse_comments(self, index):
+ index = int(index) - 1
+ return self.context.revisions[index]
+
+
class ICodeReviewDisplayComment(IComment, ICodeReviewComment):
"""Marker interface for displaying code review comments."""
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 @@
-<!-- Copyright 2009-2020 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2009-2021 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -556,6 +556,9 @@
path_expression="string:comments/${id}"
attribute_to_parent="branch_merge_proposal"
rootsite="code"/>
+ <browser:navigation
+ module=".codereviewcomment"
+ classes="CodeReviewCommentNavigation" />
<browser:defaultView
for="lp.code.interfaces.codereviewcomment.ICodeReviewComment"
name="+index"/>
diff --git a/lib/lp/security.py b/lib/lp/security.py
index cd88d5c..0ed1a2d 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -187,6 +187,7 @@ from lp.services.identity.interfaces.account import IAccount
from lp.services.identity.interfaces.emailaddress import IEmailAddress
from lp.services.librarian.interfaces import ILibraryFileAliasWithParent
from lp.services.messages.interfaces.message import IMessage
+from lp.services.messages.interfaces.messagerevision import IMessageRevision
from lp.services.oauth.interfaces import (
IOAuthAccessToken,
IOAuthRequestToken,
@@ -3191,6 +3192,16 @@ class EditMessage(AuthorizationBase):
return user.isOwner(self.obj)
+class EditMessageRevision(DelegatedAuthorization):
+ """Only the message owner can edit the message revision history."""
+ permission = 'launchpad.Edit'
+ usedfor = IMessageRevision
+
+ def __init__(self, obj):
+ super(EditMessageRevision, self).__init__(
+ obj, obj.message, 'launchpad.Edit')
+
+
class ViewPublisherConfig(AdminByAdminsTeam):
usedfor = IPublisherConfig
diff --git a/lib/lp/services/messages/browser/configure.zcml b/lib/lp/services/messages/browser/configure.zcml
new file mode 100644
index 0000000..8218040
--- /dev/null
+++ b/lib/lp/services/messages/browser/configure.zcml
@@ -0,0 +1,10 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ xmlns:i18n="http://namespaces.zope.org/i18n"
+ i18n_domain="launchpad">
+ <browser:url
+ for="lp.services.messages.interfaces.messagerevision.IMessageRevision"
+ path_expression="string:revisions/${id}"
+ attribute_to_parent="message_implementation" />
+</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
from zope.interface import implementer
+from lp.bugs.interfaces.bugmessage import IBugMessage
from lp.services.messages.interfaces.message import IIndexedMessage
from lp.services.webapp.interfaces import ICanonicalUrlData
@@ -28,6 +29,9 @@ class BugMessageCanonicalUrlData:
def __init__(self, bug, message):
self.inside = bug.default_bugtask
+ if IBugMessage.providedBy(message):
+ # bug.messages is a list of Message objects, not BugMessage.
+ message = message.message
self.path = "comments/%d" % list(bug.messages).index(message)
diff --git a/lib/lp/services/messages/configure.zcml b/lib/lp/services/messages/configure.zcml
index fcaffab..ccf8d12 100644
--- a/lib/lp/services/messages/configure.zcml
+++ b/lib/lp/services/messages/configure.zcml
@@ -75,4 +75,5 @@
/>
<webservice:register module="lp.services.messages.interfaces.webservice" />
+ <include package=".browser"/>
</configure>
diff --git a/lib/lp/services/messages/interfaces/message.py b/lib/lp/services/messages/interfaces/message.py
index ab31077..7dea2f8 100644
--- a/lib/lp/services/messages/interfaces/message.py
+++ b/lib/lp/services/messages/interfaces/message.py
@@ -88,7 +88,14 @@ class IMessageCommon(Interface):
Reference(title=_('Person'), schema=Interface,
required=False, readonly=True))
- revisions = Attribute(_('Message revision history'))
+ revisions = exported(CollectionField(
+ title=_("Message revision history"),
+ description=_(
+ "Revision history of this message, sorted in descending order."),
+ # Really IMessageRevision, patched in _schema_circular_imports.
+ value_type=Reference(schema=Interface),
+ required=False, readonly=True), as_of="devel")
+
datecreated = exported(
Datetime(title=_('Date Created'), required=True, readonly=True),
exported_as='date_created')
diff --git a/lib/lp/services/messages/interfaces/messagerevision.py b/lib/lp/services/messages/interfaces/messagerevision.py
index e940187..a3316ce 100644
--- a/lib/lp/services/messages/interfaces/messagerevision.py
+++ b/lib/lp/services/messages/interfaces/messagerevision.py
@@ -9,6 +9,12 @@ __all__ = [
'IMessageRevision'
]
+from lazr.restful.declarations import (
+ export_write_operation,
+ exported,
+ exported_as_webservice_entry,
+ operation_for_version,
+ )
from lazr.restful.fields import Reference
from zope.interface import Interface
from zope.schema import (
@@ -25,29 +31,37 @@ class IMessageRevisionView(Interface):
"""IMessageRevision readable attributes."""
id = Int(title=_("ID"), required=True, readonly=True)
- content = Text(
+ content = exported(Text(
title=_("The message at the given revision"),
- required=False, readonly=True)
+ required=False, readonly=True))
message = Reference(
title=_('The current message of this revision.'),
schema=IMessage, required=True, readonly=True)
- date_created = Datetime(
+ message_implementation = Reference(
+ title=_('The message implementation (BugComment, QuestionMessage or '
+ 'CodeReviewComment) related to this revision'),
+ schema=Interface, required=True, readonly=True)
+
+ date_created = exported(Datetime(
title=_("The time when this message revision was created."),
- required=True, readonly=True)
+ required=True, readonly=True))
- date_deleted = Datetime(
+ date_deleted = exported(Datetime(
title=_("The time when this message revision was created."),
- required=False, readonly=True)
+ required=False, readonly=True))
class IMessageRevisionEdit(Interface):
"""IMessageRevision editable attributes."""
+ @export_write_operation()
+ @operation_for_version("devel")
def deleteContent():
"""Logically deletes this MessageRevision."""
+@exported_as_webservice_entry(publish_web_link=False, as_of="devel")
class IMessageRevision(IMessageRevisionView, IMessageRevisionEdit):
"""A historical revision of a IMessage."""
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 @@
-# Copyright 2011 Canonical Ltd. This software is licensed under the
+# Copyright 2011-2021 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""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.
__metaclass__ = type
__all__ = [
'IMessage',
+ 'IMessageRevision',
]
from lp import _schema_circular_imports
from lp.services.messages.interfaces.message import IMessage
+from lp.services.messages.interfaces.messagerevision import IMessageRevision
_schema_circular_imports
diff --git a/lib/lp/services/messages/model/message.py b/lib/lp/services/messages/model/message.py
index 515710c..4960131 100644
--- a/lib/lp/services/messages/model/message.py
+++ b/lib/lp/services/messages/model/message.py
@@ -178,7 +178,7 @@ class Message(SQLBase):
return list(Store.of(self).find(
MessageRevision,
MessageRevision.message == self
- ).order_by(Desc(MessageRevision.date_created)))
+ ).order_by(MessageRevision.date_created))
def editContent(self, new_content):
"""See `IMessage`."""
diff --git a/lib/lp/services/messages/model/messagerevision.py b/lib/lp/services/messages/model/messagerevision.py
index df178e4..acbabd7 100644
--- a/lib/lp/services/messages/model/messagerevision.py
+++ b/lib/lp/services/messages/model/messagerevision.py
@@ -19,6 +19,7 @@ from storm.locals import (
)
from zope.interface import implementer
+from lp.services.database.interfaces import IStore
from lp.services.database.stormbase import StormBase
from lp.services.messages.interfaces.messagerevision import IMessageRevision
from lp.services.utils import utc_now
@@ -35,7 +36,7 @@ class MessageRevision(StormBase):
message_id = Int(name='message', allow_none=False)
message = Reference(message_id, 'Message.id')
- content = Unicode(name="content", allow_none=False)
+ content = Unicode(name="content", allow_none=True)
date_created = DateTime(
name="date_created", tzinfo=pytz.UTC, allow_none=False)
@@ -48,6 +49,27 @@ class MessageRevision(StormBase):
self.date_created = date_created
self.date_deleted = date_deleted
+ @property
+ def message_implementation(self):
+ from lp.bugs.model.bugmessage import BugMessage
+ from lp.code.model.codereviewcomment import CodeReviewComment
+ from lp.answers.model.questionmessage import QuestionMessage
+
+ store = IStore(self)
+ (identifier, ) = store.execute("""
+ SELECT 'bug' FROM BugMessage WHERE message = %s
+ UNION
+ SELECT 'question' FROM QuestionMessage WHERE message = %s
+ UNION
+ SELECT 'mp' FROM CodeReviewMessage WHERE message = %s;
+ """, params=[self.message_id] * 3).get_one()
+ id_to_class = {
+ "bug": BugMessage,
+ "question": QuestionMessage,
+ "mp": CodeReviewComment}
+ klass = id_to_class[identifier]
+ return store.find(klass, klass.message == self.message_id).one()
+
def deleteContent(self):
self.content = None
self.date_deleted = utc_now()
diff --git a/lib/lp/services/messages/tests/scenarios.py b/lib/lp/services/messages/tests/scenarios.py
new file mode 100644
index 0000000..f366574
--- /dev/null
+++ b/lib/lp/services/messages/tests/scenarios.py
@@ -0,0 +1,41 @@
+# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from testscenarios import WithScenarios
+from zope.security.proxy import ProxyFactory
+
+from lp.bugs.model.bugmessage import BugMessage
+from lp.services.database.interfaces import IStore
+from lp.testing import (
+ login_person,
+ )
+
+
+class MessageTypeScenariosMixin(WithScenarios):
+
+ scenarios = [
+ ("bug", {"message_type": "bug"}),
+ ("question", {"message_type": "question"}),
+ ("MP comment", {"message_type": "mp"})
+ ]
+
+ def setUp(self):
+ super(MessageTypeScenariosMixin, self).setUp()
+ self.person = self.factory.makePerson()
+ login_person(self.person)
+
+ def makeMessage(self, content=None, **kwargs):
+ owner = kwargs.pop('owner', self.person)
+ if self.message_type == "bug":
+ msg = self.factory.makeBugComment(
+ owner=owner, body=content, **kwargs)
+ return ProxyFactory(IStore(BugMessage).find(
+ BugMessage, BugMessage.message == msg).one())
+ elif self.message_type == "question":
+ question = self.factory.makeQuestion()
+ return question.giveAnswer(owner, content)
+ elif self.message_type == "mp":
+ return self.factory.makeCodeReviewComment(
+ 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 c65118c..a08e05a 100644
--- a/lib/lp/services/messages/tests/test_message.py
+++ b/lib/lp/services/messages/tests/test_message.py
@@ -13,7 +13,6 @@ from email.utils import (
)
import six
-from testscenarios import WithScenarios
from testtools.matchers import (
Equals,
Is,
@@ -21,23 +20,18 @@ from testtools.matchers import (
)
import transaction
from zope.security.interfaces import Unauthorized
-from zope.security.proxy import (
- ProxyFactory,
- removeSecurityProxy,
- )
+from zope.security.proxy import removeSecurityProxy
from lp.bugs.interfaces.bugmessage import IBugMessage
-from lp.bugs.model.bugmessage import BugMessage
from lp.services.compat import message_as_bytes
-from lp.services.database.interfaces import IStore
from lp.services.messages.model.message import MessageSet
+from lp.services.messages.tests.scenarios import MessageTypeScenariosMixin
from lp.services.utils import utc_now
from lp.services.webapp.interfaces import OAuthPermission
from lp.testing import (
admin_logged_in,
api_url,
login,
- login_person,
person_logged_in,
TestCaseWithFactory,
)
@@ -195,34 +189,6 @@ class TestMessageSet(TestCaseWithFactory):
self.assertEqual(self.high_characters.decode('latin-1'), result)
-class MessageTypeScenariosMixin(WithScenarios):
-
- scenarios = [
- ("bug", {"message_type": "bug"}),
- ("question", {"message_type": "question"}),
- ("MP comment", {"message_type": "mp"})
- ]
-
- def setUp(self):
- super(MessageTypeScenariosMixin, self).setUp()
- self.person = self.factory.makePerson()
- login_person(self.person)
-
- def makeMessage(self, content=None, **kwargs):
- owner = kwargs.pop('owner', self.person)
- if self.message_type == "bug":
- msg = self.factory.makeBugComment(
- owner=owner, body=content, **kwargs)
- return ProxyFactory(IStore(BugMessage).find(
- BugMessage, BugMessage.message == msg).one())
- elif self.message_type == "question":
- question = self.factory.makeQuestion()
- return question.giveAnswer(owner, content)
- elif self.message_type == "mp":
- return self.factory.makeCodeReviewComment(
- sender=owner, body=content)
-
-
class TestMessageEditing(MessageTypeScenariosMixin, TestCaseWithFactory):
"""Test editing scenarios for Message objects."""
@@ -273,10 +239,10 @@ class TestMessageEditing(MessageTypeScenariosMixin, TestCaseWithFactory):
self.assertEqual("final form", msg.text_contents)
self.assertEqual(2, len(msg.revisions))
self.assertIsMessageHistory(
- msg.revisions[0], msg,
+ msg.revisions[1], msg,
content="first edit", created_at=first_edit_date)
self.assertIsMessageHistory(
- msg.revisions[1], msg,
+ msg.revisions[0], msg,
content="initial content", created_at=msg.datecreated)
def test_non_owner_cannot_delete_message(self):
diff --git a/lib/lp/services/messages/tests/test_messagerevision.py b/lib/lp/services/messages/tests/test_messagerevision.py
new file mode 100644
index 0000000..18da29c
--- /dev/null
+++ b/lib/lp/services/messages/tests/test_messagerevision.py
@@ -0,0 +1,127 @@
+# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from testtools.matchers import (
+ ContainsDict,
+ EndsWith,
+ Equals,
+ Is,
+ MatchesListwise,
+ Not,
+ )
+
+from lp.bugs.interfaces.bugmessage import IBugMessage
+from lp.services.messages.tests.scenarios import MessageTypeScenariosMixin
+from lp.services.webapp.interfaces import OAuthPermission
+from lp.testing import (
+ admin_logged_in,
+ api_url,
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.pages import webservice_for_person
+
+
+class TestMessageHistoryAPI(MessageTypeScenariosMixin, TestCaseWithFactory):
+ """Test editing scenarios for message revisions API."""
+
+ layer = DatabaseFunctionalLayer
+
+ def getWebservice(self, person):
+ return webservice_for_person(
+ person, permission=OAuthPermission.WRITE_PUBLIC,
+ default_api_version="devel")
+
+ def getMessageAPIURL(self, msg):
+ with admin_logged_in():
+ if IBugMessage.providedBy(msg):
+ # BugMessage has a special URL mapping that uses the
+ # IMessage object itself.
+ return api_url(msg.message)
+ else:
+ return api_url(msg)
+
+ def test_get_message_revision_list(self):
+ msg = self.makeMessage(content="initial content")
+ msg.editContent("new content 1")
+ msg.editContent("final content")
+ ws = self.getWebservice(self.person)
+ url = self.getMessageAPIURL(msg)
+ ws_message = ws.get(url).jsonBody()
+
+ revisions = ws.get(ws_message['revisions_collection_link']).jsonBody()
+ self.assertThat(revisions, ContainsDict({
+ "start": Equals(0),
+ "total_size": Equals(2)}))
+ self.assertThat(revisions["entries"], MatchesListwise([
+ ContainsDict({
+ "date_created": Not(Is(None)),
+ "date_deleted": Is(None),
+ "content": Equals("initial content"),
+ "self_link": EndsWith("/revisions/1")
+ }),
+ ContainsDict({
+ "date_created": Not(Is(None)),
+ "date_deleted": Is(None),
+ "content": Equals("new content 1"),
+ "self_link": EndsWith("/revisions/2")
+ })]))
+
+ def test_get_single_revision(self):
+ msg = self.makeMessage(content="initial content")
+ msg.editContent("new content 1")
+ ws = self.getWebservice(self.person)
+
+ with person_logged_in(self.person):
+ revision_url = api_url(msg.revisions[0])
+ revision = ws.get(revision_url).jsonBody()
+ self.assertThat(revision, ContainsDict({
+ "date_created": Not(Is(None)),
+ "date_deleted": Is(None),
+ "content": Equals("initial content"),
+ "self_link": EndsWith("/revisions/1")
+ }))
+
+ def test_delete_revision_content(self):
+ msg = self.makeMessage(content="initial content")
+ msg.editContent("new content 1")
+ msg.editContent("final content")
+
+ with person_logged_in(self.person):
+ revision_url = api_url(msg.revisions[0])
+
+ ws = self.getWebservice(self.person)
+ response = ws.named_post(revision_url, "deleteContent")
+ self.assertEqual(200, response.status)
+
+ revision = ws.get(revision_url).jsonBody()
+ self.assertThat(revision, ContainsDict({
+ "date_created": Not(Is(None)),
+ "date_deleted": Not(Is(None)),
+ "content": Is(None),
+ "self_link": EndsWith("/revisions/1")
+ }))
+
+ def test_delete_revision_content_denied_for_non_owners(self):
+ msg = self.makeMessage(content="initial content")
+ msg.editContent("new content 1")
+ msg.editContent("final content")
+ someone_else = self.factory.makePerson()
+
+ with person_logged_in(self.person):
+ revision_url = api_url(msg.revisions[0])
+
+ ws = self.getWebservice(someone_else)
+ response = ws.named_post(revision_url, "deleteContent")
+ self.assertEqual(401, response.status)
+
+ revision = ws.get(revision_url).jsonBody()
+ self.assertThat(revision, ContainsDict({
+ "date_created": Not(Is(None)),
+ "date_deleted": Is(None),
+ "content": Equals("initial content"),
+ "self_link": EndsWith("/revisions/1")
+ }))
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 @@
<xsl:text>/comments/</xsl:text>
<var><index></var>
</xsl:when>
+ <xsl:when test="@id = 'message_revision'">
+ <xsl:text>/</xsl:text>
+ <var><message-url></var>
+ <xsl:text>/revisions/</xsl:text>
+ <var><index></var>
+ </xsl:when>
<xsl:when test="@id = 'milestone'">
<xsl:text>/</xsl:text>
<var><target.name></var>
Follow ups