[Launchpad-reviewers] [Merge] ~pappacena/launchpad:comment-editing-model into launchpad:master

2021-05-20 Thread noreply
The proposal to merge ~pappacena/launchpad:comment-editing-model into 
launchpad:master has been updated.

Status: Approved => Merged

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/401894
-- 
Your team Launchpad code reviewers is subscribed to branch 
~pappacena/launchpad:comment-editing-model.

___
Mailing list: https://launchpad.net/~launchpad-reviewers
Post to : launchpad-reviewers@lists.launchpad.net
Unsubscribe : https://launchpad.net/~launchpad-reviewers
More help   : https://help.launchpad.net/ListHelp


[Launchpad-reviewers] [Merge] ~pappacena/launchpad:comment-editing-model into launchpad:master

2021-05-20 Thread Thiago F. Pappacena
The proposal to merge ~pappacena/launchpad:comment-editing-model into 
launchpad:master has been updated.

Status: Needs review => Approved

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/401894
-- 
Your team Launchpad code reviewers is subscribed to branch 
~pappacena/launchpad:comment-editing-model.

___
Mailing list: https://launchpad.net/~launchpad-reviewers
Post to : launchpad-reviewers@lists.launchpad.net
Unsubscribe : https://launchpad.net/~launchpad-reviewers
More help   : https://help.launchpad.net/ListHelp


Re: [Launchpad-reviewers] [Merge] ~pappacena/launchpad:comment-editing-model into launchpad:master

2021-05-20 Thread Thiago F. Pappacena
Pushed the requested changes. This should be good to go now.

Diff comments:

> diff --git a/lib/lp/services/messages/interfaces/messagerevision.py 
> b/lib/lp/services/messages/interfaces/messagerevision.py
> new file mode 100644
> index 000..3b72673
> --- /dev/null
> +++ b/lib/lp/services/messages/interfaces/messagerevision.py
> @@ -0,0 +1,69 @@
> +# Copyright 2019-2021 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Message revision history."""
> +
> +from __future__ import absolute_import, print_function, unicode_literals
> +
> +__all__ = [
> +'IMessageRevision',
> +'IMessageRevisionChunk',
> +]
> +
> +from lazr.restful.fields import Reference
> +from zope.interface import (
> +Attribute,
> +Interface,
> +)
> +from zope.schema import (
> +Datetime,
> +Int,
> +Text,
> +)
> +
> +from lp import _
> +from lp.services.messages.interfaces.message import IMessage
> +
> +
> +class IMessageRevisionView(Interface):
> +"""IMessageRevision readable attributes."""
> +id = Int(title=_("ID"), required=True, readonly=True)
> +
> +revision = Int(title=_("Revision number"), required=True, readonly=True)
> +
> +content = Text(
> +title=_("The message at the given revision"),
> +required=False, readonly=True)

:facepalm:
Fixing it.

> +
> +message = Reference(
> +title=_('The current message of this revision.'),
> +schema=IMessage, required=True, readonly=True)
> +
> +date_created = Datetime(
> +title=_("The time when this message revision was created."),
> +required=True, readonly=True)
> +
> +date_deleted = Datetime(
> +title=_("The time when this message revision was created."),
> +required=False, readonly=True)
> +
> +chunks = Attribute(_('Message pieces'))
> +
> +
> +class IMessageRevisionEdit(Interface):
> +"""IMessageRevision editable attributes."""
> +
> +def deleteContent():
> +"""Logically deletes this MessageRevision."""
> +
> +
> +class IMessageRevision(IMessageRevisionView, IMessageRevisionEdit):
> +"""A historical revision of a IMessage."""
> +
> +
> +class IMessageRevisionChunk(Interface):
> +id = Int(title=_('ID'), required=True, readonly=True)
> +messagerevision = Int(
> +title=_('MessageRevision'), required=True, readonly=True)
> +sequence = Int(title=_('Sequence order'), required=True, readonly=True)
> +content = Text(title=_('Text content'), required=False, readonly=True)
> diff --git a/lib/lp/services/messages/model/message.py 
> b/lib/lp/services/messages/model/message.py
> index e592daa..b73e212 100644
> --- a/lib/lp/services/messages/model/message.py
> +++ b/lib/lp/services/messages/model/message.py
> @@ -164,6 +176,63 @@ class Message(SQLBase):
>  """See `IMessage`."""
>  return None
>  
> +@cachedproperty
> +def revisions(self):
> +"""See `IMessage`."""
> +return list(Store.of(self).find(
> +MessageRevision,
> +MessageRevision.message == self
> +).order_by(Desc(MessageRevision.revision)))
> +
> +def editContent(self, new_content):
> +"""See `IMessage`."""
> +store = Store.of(self)
> +
> +# Move the old content to a new revision.
> +date_created = (
> +self.date_last_edited if self.date_last_edited is not None
> +else self.datecreated)
> +current_rev_num = store.find(
> +Max(MessageRevision.revision),
> +MessageRevision.message == self).one()
> +rev_num = (current_rev_num or 0) + 1
> +rev = MessageRevision(
> +message=self, revision=rev_num, date_created=date_created)
> +self.date_last_edited = utc_now()
> +store.add(rev)
> +
> +# Move the current text content to the recently created revision.
> +for chunk in self._chunks:
> +if chunk.blob is None:
> +revision_chunk = MessageRevisionChunk(
> +rev, chunk.sequence, chunk.content)
> +store.add(revision_chunk)
> +store.remove(chunk)
> +
> +# Create the new content.
> +new_chunk = MessageChunk(message=self, sequence=1, 
> content=new_content)

Ok! I'll try to fill eventual gaps instead of jumping directly into max_seq + 1.

> +store.add(new_chunk)
> +
> +store.flush()
> +
> +# Clean up caches.
> +del get_property_cache(self).text_contents
> +del get_property_cache(self).chunks
> +del get_property_cache(self).revisions
> +return rev
> +
> +def deleteContent(self):
> +"""See `IMessage`."""
> +store = Store.of(self)
> +store.find(MessageChunk, MessageChunk.message == self).remove()
> +for rev in self.revisions:
> +store.find(MessageRevisionChunk, 

Re: [Launchpad-reviewers] [Merge] ~pappacena/launchpad:comment-editing-model into launchpad:master

2021-05-20 Thread Colin Watson
Review: Approve



Diff comments:

> diff --git a/lib/lp/services/messages/interfaces/messagerevision.py 
> b/lib/lp/services/messages/interfaces/messagerevision.py
> new file mode 100644
> index 000..3b72673
> --- /dev/null
> +++ b/lib/lp/services/messages/interfaces/messagerevision.py
> @@ -0,0 +1,69 @@
> +# Copyright 2019-2021 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Message revision history."""
> +
> +from __future__ import absolute_import, print_function, unicode_literals
> +
> +__all__ = [
> +'IMessageRevision',
> +'IMessageRevisionChunk',
> +]
> +
> +from lazr.restful.fields import Reference
> +from zope.interface import (
> +Attribute,
> +Interface,
> +)
> +from zope.schema import (
> +Datetime,
> +Int,
> +Text,
> +)
> +
> +from lp import _
> +from lp.services.messages.interfaces.message import IMessage
> +
> +
> +class IMessageRevisionView(Interface):
> +"""IMessageRevision readable attributes."""
> +id = Int(title=_("ID"), required=True, readonly=True)
> +
> +revision = Int(title=_("Revision number"), required=True, readonly=True)
> +
> +content = Text(
> +title=_("The message at the given revision"),
> +required=False, readonly=True)

It looks like you changed `IMessageRevisionChunk.content` but forgot to change 
this one?

> +
> +message = Reference(
> +title=_('The current message of this revision.'),
> +schema=IMessage, required=True, readonly=True)
> +
> +date_created = Datetime(
> +title=_("The time when this message revision was created."),
> +required=True, readonly=True)
> +
> +date_deleted = Datetime(
> +title=_("The time when this message revision was created."),
> +required=False, readonly=True)
> +
> +chunks = Attribute(_('Message pieces'))
> +
> +
> +class IMessageRevisionEdit(Interface):
> +"""IMessageRevision editable attributes."""
> +
> +def deleteContent():
> +"""Logically deletes this MessageRevision."""
> +
> +
> +class IMessageRevision(IMessageRevisionView, IMessageRevisionEdit):
> +"""A historical revision of a IMessage."""
> +
> +
> +class IMessageRevisionChunk(Interface):
> +id = Int(title=_('ID'), required=True, readonly=True)
> +messagerevision = Int(
> +title=_('MessageRevision'), required=True, readonly=True)
> +sequence = Int(title=_('Sequence order'), required=True, readonly=True)
> +content = Text(title=_('Text content'), required=False, readonly=True)
> diff --git a/lib/lp/services/messages/model/message.py 
> b/lib/lp/services/messages/model/message.py
> index e592daa..b73e212 100644
> --- a/lib/lp/services/messages/model/message.py
> +++ b/lib/lp/services/messages/model/message.py
> @@ -164,6 +176,63 @@ class Message(SQLBase):
>  """See `IMessage`."""
>  return None
>  
> +@cachedproperty
> +def revisions(self):
> +"""See `IMessage`."""
> +return list(Store.of(self).find(
> +MessageRevision,
> +MessageRevision.message == self
> +).order_by(Desc(MessageRevision.revision)))
> +
> +def editContent(self, new_content):
> +"""See `IMessage`."""
> +store = Store.of(self)
> +
> +# Move the old content to a new revision.
> +date_created = (
> +self.date_last_edited if self.date_last_edited is not None
> +else self.datecreated)
> +current_rev_num = store.find(
> +Max(MessageRevision.revision),
> +MessageRevision.message == self).one()
> +rev_num = (current_rev_num or 0) + 1
> +rev = MessageRevision(
> +message=self, revision=rev_num, date_created=date_created)
> +self.date_last_edited = utc_now()
> +store.add(rev)
> +
> +# Move the current text content to the recently created revision.
> +for chunk in self._chunks:
> +if chunk.blob is None:
> +revision_chunk = MessageRevisionChunk(
> +rev, chunk.sequence, chunk.content)
> +store.add(revision_chunk)
> +store.remove(chunk)
> +
> +# Create the new content.
> +new_chunk = MessageChunk(message=self, sequence=1, 
> content=new_content)

It's probably not vital, but I think ideally the new chunk should occupy the 
smallest unused sequence number, rather than the largest + 1.  (Consider [text 
comment, binary attachment], which if edited should probably turn into [new 
text comment, binary attachment] rather than [gap, binary attachment, new text 
comment].)

> +store.add(new_chunk)
> +
> +store.flush()
> +
> +# Clean up caches.
> +del get_property_cache(self).text_contents
> +del get_property_cache(self).chunks
> +del get_property_cache(self).revisions
> +return rev
> +
> +def 

Re: [Launchpad-reviewers] [Merge] ~pappacena/launchpad:comment-editing-model into launchpad:master

2021-05-14 Thread Thiago F. Pappacena
Addressed all the comments.

Diff comments:

> diff --git a/database/schema/security.cfg b/database/schema/security.cfg
> index c3aba65..b712521 100644
> --- a/database/schema/security.cfg
> +++ b/database/schema/security.cfg
> @@ -232,7 +232,9 @@ public.logintoken   = SELECT, INSERT, 
> UPDATE, DELETE
>  public.mailinglist  = SELECT, INSERT, UPDATE, DELETE
>  public.mailinglistsubscription  = SELECT, INSERT, UPDATE, DELETE
>  public.messageapproval  = SELECT, INSERT, UPDATE, DELETE
> -public.messagechunk = SELECT, INSERT
> +public.messagechunk = SELECT, INSERT, DELETE
> +public.messagerevision  = SELECT, INSERT, UPDATE, DELETE
> +public.messagerevisionchunk = SELECT, INSERT, UPDATE, DELETE

You are right. Fixing it.

>  public.milestone= SELECT, INSERT, UPDATE, DELETE
>  public.milestonetag = SELECT, INSERT, UPDATE, DELETE
>  public.mirrorcdimagedistroseries= SELECT, INSERT, DELETE
> diff --git a/lib/lp/security.py b/lib/lp/security.py
> index 412f497..cd88d5c 100644
> --- a/lib/lp/security.py
> +++ b/lib/lp/security.py
> @@ -3182,6 +3182,15 @@ class SetMessageVisibility(AuthorizationBase):
>  return (user.in_admin or user.in_registry_experts)
>  
>  
> +class EditMessage(AuthorizationBase):
> +permission = 'launchpad.Edit'
> +usedfor = IMessage
> +
> +def checkAuthenticated(self, user):
> +"""Only message owner can edit it."""
> +return user.isOwner(self.obj)

Right. Adding it.

> +
> +
>  class ViewPublisherConfig(AdminByAdminsTeam):
>  usedfor = IPublisherConfig
>  
> diff --git a/lib/lp/services/messages/interfaces/messagerevision.py 
> b/lib/lp/services/messages/interfaces/messagerevision.py
> new file mode 100644
> index 000..3b72673
> --- /dev/null
> +++ b/lib/lp/services/messages/interfaces/messagerevision.py
> @@ -0,0 +1,69 @@
> +# Copyright 2019-2021 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Message revision history."""
> +
> +from __future__ import absolute_import, print_function, unicode_literals
> +
> +__all__ = [
> +'IMessageRevision',
> +'IMessageRevisionChunk',
> +]
> +
> +from lazr.restful.fields import Reference
> +from zope.interface import (
> +Attribute,
> +Interface,
> +)
> +from zope.schema import (
> +Datetime,
> +Int,
> +Text,
> +)
> +
> +from lp import _
> +from lp.services.messages.interfaces.message import IMessage
> +
> +
> +class IMessageRevisionView(Interface):
> +"""IMessageRevision readable attributes."""
> +id = Int(title=_("ID"), required=True, readonly=True)
> +
> +revision = Int(title=_("Revision number"), required=True, readonly=True)
> +
> +content = Text(
> +title=_("The message at the given revision"),
> +required=False, readonly=True)

Ok!

> +
> +message = Reference(
> +title=_('The current message of this revision.'),
> +schema=IMessage, required=True, readonly=True)
> +
> +date_created = Datetime(
> +title=_("The time when this message revision was created."),
> +required=True, readonly=True)
> +
> +date_deleted = Datetime(
> +title=_("The time when this message revision was created."),
> +required=False, readonly=True)
> +
> +chunks = Attribute(_('Message pieces'))
> +
> +
> +class IMessageRevisionEdit(Interface):
> +"""IMessageRevision editable attributes."""
> +
> +def deleteContent():
> +"""Logically deletes this MessageRevision."""

Fixing this description.

> +
> +
> +class IMessageRevision(IMessageRevisionView, IMessageRevisionEdit):
> +"""A historical revision of a IMessage."""
> +
> +
> +class IMessageRevisionChunk(Interface):
> +id = Int(title=_('ID'), required=True, readonly=True)
> +messagerevision = Int(
> +title=_('MessageRevision'), required=True, readonly=True)
> +sequence = Int(title=_('Sequence order'), required=True, readonly=True)
> +content = Text(title=_('Text content'), required=False, readonly=True)

Ok!

> diff --git a/lib/lp/services/messages/model/message.py 
> b/lib/lp/services/messages/model/message.py
> index e592daa..b73e212 100644
> --- a/lib/lp/services/messages/model/message.py
> +++ b/lib/lp/services/messages/model/message.py
> @@ -164,6 +176,63 @@ class Message(SQLBase):
>  """See `IMessage`."""
>  return None
>  
> +@cachedproperty
> +def revisions(self):
> +"""See `IMessage`."""
> +return list(Store.of(self).find(
> +MessageRevision,
> +MessageRevision.message == self
> +).order_by(Desc(MessageRevision.revision)))
> +
> +def editContent(self, new_content):
> +"""See `IMessage`."""
> +store = Store.of(self)
> +
> +# Move the old 

Re: [Launchpad-reviewers] [Merge] ~pappacena/launchpad:comment-editing-model into launchpad:master

2021-05-14 Thread Colin Watson
Review: Approve



Diff comments:

> diff --git a/database/schema/security.cfg b/database/schema/security.cfg
> index c3aba65..b712521 100644
> --- a/database/schema/security.cfg
> +++ b/database/schema/security.cfg
> @@ -232,7 +232,9 @@ public.logintoken   = SELECT, INSERT, 
> UPDATE, DELETE
>  public.mailinglist  = SELECT, INSERT, UPDATE, DELETE
>  public.mailinglistsubscription  = SELECT, INSERT, UPDATE, DELETE
>  public.messageapproval  = SELECT, INSERT, UPDATE, DELETE
> -public.messagechunk = SELECT, INSERT
> +public.messagechunk = SELECT, INSERT, DELETE
> +public.messagerevision  = SELECT, INSERT, UPDATE, DELETE
> +public.messagerevisionchunk = SELECT, INSERT, UPDATE, DELETE

Are `MessageRevisionChunk` rows ever updated?  I thought they were only 
inserted or deleted.

>  public.milestone= SELECT, INSERT, UPDATE, DELETE
>  public.milestonetag = SELECT, INSERT, UPDATE, DELETE
>  public.mirrorcdimagedistroseries= SELECT, INSERT, DELETE
> diff --git a/lib/lp/security.py b/lib/lp/security.py
> index 412f497..cd88d5c 100644
> --- a/lib/lp/security.py
> +++ b/lib/lp/security.py
> @@ -3182,6 +3182,15 @@ class SetMessageVisibility(AuthorizationBase):
>  return (user.in_admin or user.in_registry_experts)
>  
>  
> +class EditMessage(AuthorizationBase):
> +permission = 'launchpad.Edit'
> +usedfor = IMessage
> +
> +def checkAuthenticated(self, user):
> +"""Only message owner can edit it."""
> +return user.isOwner(self.obj)

I guess there's some kind of default permission that applies to 
`IMessageRevision`, but it seems as though it might be a good idea for it to 
explicitly delegate to its parent `IMessage`.

> +
> +
>  class ViewPublisherConfig(AdminByAdminsTeam):
>  usedfor = IPublisherConfig
>  
> diff --git a/lib/lp/services/messages/interfaces/messagerevision.py 
> b/lib/lp/services/messages/interfaces/messagerevision.py
> new file mode 100644
> index 000..3b72673
> --- /dev/null
> +++ b/lib/lp/services/messages/interfaces/messagerevision.py
> @@ -0,0 +1,69 @@
> +# Copyright 2019-2021 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Message revision history."""
> +
> +from __future__ import absolute_import, print_function, unicode_literals
> +
> +__all__ = [
> +'IMessageRevision',
> +'IMessageRevisionChunk',
> +]
> +
> +from lazr.restful.fields import Reference
> +from zope.interface import (
> +Attribute,
> +Interface,
> +)
> +from zope.schema import (
> +Datetime,
> +Int,
> +Text,
> +)
> +
> +from lp import _
> +from lp.services.messages.interfaces.message import IMessage
> +
> +
> +class IMessageRevisionView(Interface):
> +"""IMessageRevision readable attributes."""
> +id = Int(title=_("ID"), required=True, readonly=True)
> +
> +revision = Int(title=_("Revision number"), required=True, readonly=True)
> +
> +content = Text(
> +title=_("The message at the given revision"),
> +required=False, readonly=True)

Maybe `required=True`, since the property can never return None?

> +
> +message = Reference(
> +title=_('The current message of this revision.'),
> +schema=IMessage, required=True, readonly=True)
> +
> +date_created = Datetime(
> +title=_("The time when this message revision was created."),
> +required=True, readonly=True)
> +
> +date_deleted = Datetime(
> +title=_("The time when this message revision was created."),
> +required=False, readonly=True)
> +
> +chunks = Attribute(_('Message pieces'))
> +
> +
> +class IMessageRevisionEdit(Interface):
> +"""IMessageRevision editable attributes."""
> +
> +def deleteContent():
> +"""Logically deletes this MessageRevision."""

What does "Logically deletes" mean?

> +
> +
> +class IMessageRevision(IMessageRevisionView, IMessageRevisionEdit):
> +"""A historical revision of a IMessage."""
> +
> +
> +class IMessageRevisionChunk(Interface):
> +id = Int(title=_('ID'), required=True, readonly=True)
> +messagerevision = Int(
> +title=_('MessageRevision'), required=True, readonly=True)
> +sequence = Int(title=_('Sequence order'), required=True, readonly=True)
> +content = Text(title=_('Text content'), required=False, readonly=True)

Should be `required=True` to match the DB patch, I think.

> diff --git a/lib/lp/services/messages/model/message.py 
> b/lib/lp/services/messages/model/message.py
> index e592daa..b73e212 100644
> --- a/lib/lp/services/messages/model/message.py
> +++ b/lib/lp/services/messages/model/message.py
> @@ -164,6 +176,63 @@ class Message(SQLBase):
>  """See `IMessage`."""
>  return None
>  
> +@cachedproperty
> +def revisions(self):
> +   

[Launchpad-reviewers] [Merge] ~pappacena/launchpad:comment-editing-model into launchpad:master

2021-05-05 Thread Thiago F. Pappacena
The proposal to merge ~pappacena/launchpad:comment-editing-model into 
launchpad:master has been updated.

Description changed to:

More changes will be added in a future MP in order to adjust BugComment, 
CodeReviewComment and QuestionMessage to use the new methods. These changes 
were splitted to make the review process easier.

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/401894
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of 
~pappacena/launchpad:comment-editing-model into launchpad:master.

___
Mailing list: https://launchpad.net/~launchpad-reviewers
Post to : launchpad-reviewers@lists.launchpad.net
Unsubscribe : https://launchpad.net/~launchpad-reviewers
More help   : https://help.launchpad.net/ListHelp


[Launchpad-reviewers] [Merge] ~pappacena/launchpad:comment-editing-model into launchpad:master

2021-05-05 Thread Thiago F. Pappacena
The proposal to merge ~pappacena/launchpad:comment-editing-model into 
launchpad:master has been updated.

Description changed to:

More changes will be added in a future MP in order to adjust BugComment, 
CodeReviewComment and QuestionMessage to use the new methods. Those changes 
were splitted to make the review process easier.

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/401894
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of 
~pappacena/launchpad:comment-editing-model into launchpad:master.

___
Mailing list: https://launchpad.net/~launchpad-reviewers
Post to : launchpad-reviewers@lists.launchpad.net
Unsubscribe : https://launchpad.net/~launchpad-reviewers
More help   : https://help.launchpad.net/ListHelp


[Launchpad-reviewers] [Merge] ~pappacena/launchpad:comment-editing-model into launchpad:master

2021-04-28 Thread Thiago F. Pappacena
Thiago F. Pappacena has proposed merging 
~pappacena/launchpad:comment-editing-model into launchpad:master.

Commit message:
Mapping database initial structure for message editing

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/401894
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of 
~pappacena/launchpad:comment-editing-model into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index 1bbb845..c8b7d10 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -232,7 +232,8 @@ public.logintoken   = SELECT, INSERT, UPDATE, DELETE
 public.mailinglist  = SELECT, INSERT, UPDATE, DELETE
 public.mailinglistsubscription  = SELECT, INSERT, UPDATE, DELETE
 public.messageapproval  = SELECT, INSERT, UPDATE, DELETE
-public.messagechunk = SELECT, INSERT
+public.messagechunk = SELECT, INSERT, DELETE
+public.messagerevision  = SELECT, INSERT, UPDATE
 public.milestone= SELECT, INSERT, UPDATE, DELETE
 public.milestonetag = SELECT, INSERT, UPDATE, DELETE
 public.mirrorcdimagedistroseries= SELECT, INSERT, DELETE
diff --git a/lib/lp/bugs/browser/tests/test_bugcomment.py b/lib/lp/bugs/browser/tests/test_bugcomment.py
index c1877a5..e5baac1 100644
--- a/lib/lp/bugs/browser/tests/test_bugcomment.py
+++ b/lib/lp/bugs/browser/tests/test_bugcomment.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2018 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).
 
 """Tests for the bugcomment module."""
@@ -35,6 +35,7 @@ from lp.testing import (
 BrowserTestCase,
 celebrity_logged_in,
 login_person,
+person_logged_in,
 TestCase,
 TestCaseWithFactory,
 verifyObject,
@@ -300,7 +301,10 @@ class TestBugCommentImplementsInterface(TestCaseWithFactory):
 bug_message = self.factory.makeBugComment()
 bugtask = bug_message.bugs[0].bugtasks[0]
 bug_comment = BugComment(1, bug_message, bugtask)
-verifyObject(IBugComment, bug_comment)
+# Runs verifyObject logged in as the bug owner, so we don't fail on
+# attributes that are not public to everyone.
+with person_logged_in(bug_message.owner):
+verifyObject(IBugComment, bug_comment)
 
 def test_download_url(self):
 """download_url is provided and works as expected."""
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 412f497..cd88d5c 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -3182,6 +3182,15 @@ class SetMessageVisibility(AuthorizationBase):
 return (user.in_admin or user.in_registry_experts)
 
 
+class EditMessage(AuthorizationBase):
+permission = 'launchpad.Edit'
+usedfor = IMessage
+
+def checkAuthenticated(self, user):
+"""Only message owner can edit it."""
+return user.isOwner(self.obj)
+
+
 class ViewPublisherConfig(AdminByAdminsTeam):
 usedfor = IPublisherConfig
 
diff --git a/lib/lp/services/messages/configure.zcml b/lib/lp/services/messages/configure.zcml
index e989fe2..fcaffab 100644
--- a/lib/lp/services/messages/configure.zcml
+++ b/lib/lp/services/messages/configure.zcml
@@ -1,4 +1,4 @@
-
 
@@ -10,9 +10,9 @@
 i18n_domain="launchpad">
 
 
-
-
+
+
 
@@ -22,6 +22,19 @@
 
+
+
+
+
+
+
+
 
 
 
diff --git a/lib/lp/services/messages/interfaces/message.py b/lib/lp/services/messages/interfaces/message.py
index 7e18e7d..0c54c84 100644
--- a/lib/lp/services/messages/interfaces/message.py
+++ b/lib/lp/services/messages/interfaces/message.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 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).
 
 __metaclass__ = type
@@ -49,9 +49,19 @@ from lp.services.librarian.interfaces import ILibraryFileAlias
 from lp.services.webservice.apihelpers import patch_reference_property
 
 
-@exported_as_webservice_entry('message')
-class IMessage(Interface):
-"""A message.
+class IMessageEdit(Interface):
+
+def edit_content(new_content):
+"""Edit the content of this message, generating a new message
+revision with the old content.
+"""
+
+def delete_content():
+"""Deletes this message content."""
+
+
+class IMessageView(Interface):
+"""Public attributes for message.
 
 This is like an email (RFC822) message, though it could be created through
 the web as well.
@@ -61,6 +71,15 @@ class IMessage(Interface):