Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Commit

Permalink
support PM delete opration on Recipient side only
Browse files Browse the repository at this point in the history
  • Loading branch information
jackniu1 committed May 18, 2016
1 parent 90ce14b commit 7344336
Show file tree
Hide file tree
Showing 16 changed files with 749 additions and 4 deletions.
25 changes: 25 additions & 0 deletions r2/r2/controllers/api.py
Expand Up @@ -1723,6 +1723,31 @@ def POST_report(self, form, jquery, thing, reason, site_reason, other_reason):
parent_div.removeClass("active")
parent_div.html("")

@require_oauth2_scope("privatemessages")
@noresponse(
VUser(),
VModhash(),
thing=VByName('id'),
)
@api_doc(api_section.messages)
def POST_del_msg(self, thing):
"""Delete messages from the recipient's view of their inbox."""
if not thing:
return

if not isinstance(thing, Message):
return

if thing.to_id != c.user._id:
return

thing.del_on_recipient = True
thing._commit()

# report the message deletion to data pipeline
g.events.message_event(thing, "ss.delete_message",
request=request, context=c)

@require_oauth2_scope("privatemessages")
@noresponse(
VUser(),
Expand Down
5 changes: 5 additions & 0 deletions r2/r2/controllers/listingcontroller.py
Expand Up @@ -1128,6 +1128,11 @@ def keep(item):
if item.author_id in c.user.enemies:
return False

# do not show messages which were deleted on recipient
if (isinstance(item, Message) and
item.to_id == c.user._id and item.del_on_recipient):
return False

if item.author_id == c.user._id:
return wouldkeep

Expand Down
5 changes: 3 additions & 2 deletions r2/r2/lib/eventcollector.py
Expand Up @@ -571,7 +571,7 @@ def modmail_event(self, message, request=None, context=None):

event = Event(
topic="message_events",
event_type="ss.send_message",
event_type=event_type,
time=message._date,
request=request,
context=context,
Expand Down Expand Up @@ -624,7 +624,8 @@ def modmail_event(self, message, request=None, context=None):

@squelch_exceptions
@sampled("events_collector_message_sample_rate")
def message_event(self, message, request=None, context=None):
def message_event(self, message, event_type="ss.send_message",
request=None, context=None):
"""Create a 'message' event for event-collector.
message: An r2.models.Message object
Expand Down
3 changes: 3 additions & 0 deletions r2/r2/lib/pages/things.py
Expand Up @@ -279,6 +279,8 @@ def __init__(self, thing, delete = False, report = True):
can_block = True
can_mute = False
is_admin_message = False
del_on_recipient = (isinstance(thing, Message) and
thing.del_on_recipient)

if not was_comment:
first_message = thing
Expand Down Expand Up @@ -322,6 +324,7 @@ def __init__(self, thing, delete = False, report = True):
can_block = can_block,
can_mute = can_mute,
is_admin_message = is_admin_message,
del_on_recipient=del_on_recipient,
)


Expand Down
6 changes: 6 additions & 0 deletions r2/r2/models/builder.py
Expand Up @@ -1853,6 +1853,12 @@ def _viewable_message(self, message):

if message.author_id in self.user.enemies:
return False

# do not show messages which were deleted on recipient
if (hasattr(message, "del_on_recipient") and
message.to_id == c.user._id and message.del_on_recipient):
return False

return super(UserMessageBuilder, self)._viewable_message(message)

def get_tree(self):
Expand Down
9 changes: 8 additions & 1 deletion r2/r2/models/link.py
Expand Up @@ -1996,6 +1996,7 @@ class Message(Thing, Printable):
display_to=None,
email_id=None,
sent_via_email=False,
del_on_recipient=False,
)
_data_int_props = Thing._data_int_props + ('reported',)
_essentials = ('author_id',)
Expand Down Expand Up @@ -2471,7 +2472,13 @@ def wrapped_cache_key(wrapped, style):
return s

def keep_item(self, wrapped):
return c.user_is_admin or not wrapped.enemy
if c.user_is_admin:
return True
# do not keep message which were deleted on recipient
if (isinstance(self, Message) and
self.to_id == c.user._id and self.del_on_recipient):
return False
return not wrapped.enemy


class _SaveHideByAccount(tdb_cassandra.DenormalizedRelation):
Expand Down
8 changes: 8 additions & 0 deletions r2/r2/templates/message.html
Expand Up @@ -83,6 +83,14 @@
${"[%s]" % ("+" if thing.collapsed else "–")}
</a>

%if c.user_is_admin:
%if not thing.was_comment and thing.del_on_recipient:
<em>${_("deleted message by")}</em>&#32;
%endif
${WrappedUser(thing.to, thing.attribs, thing)}
&#32;
%endif

<%
substitutions = {}

Expand Down
6 changes: 6 additions & 0 deletions r2/r2/templates/printablebuttons.html
Expand Up @@ -544,6 +544,12 @@
</li>
%endif
%if thing.user_is_recipient:
## only allow message deleting on recipient side now
%if not (thing.was_comment or thing.thing.del_on_recipient):
<li>
${ynbutton(_("delete"), _("deleted"), "del_msg", "hide_thing", access_required=False, event_action="delete_message")}
</li>
%endif
%if thing.can_block:
${self.banbuttons()}
%if thing.thing.author_id != c.user._id and thing.thing.author_id not in c.user.enemies:
Expand Down
49 changes: 48 additions & 1 deletion r2/r2/tests/__init__.py
Expand Up @@ -37,7 +37,11 @@
import paste.script.appinstall
from paste.deploy import loadapp

__all__ = ['RedditTestCase']
from routes.util import url_for
from r2.lib.utils import query_string


__all__ = ['RedditTestCase', 'RedditControllerTestCase']

here_dir = os.path.dirname(os.path.abspath(__file__))
conf_dir = os.path.dirname(os.path.dirname(here_dir))
Expand Down Expand Up @@ -214,6 +218,8 @@ def set_multi(self, *a, **kw):


class RedditControllerTestCase(RedditTestCase):
CONTROLLER = None
ACTIONS = {}

def setUp(self):
super(RedditControllerTestCase, self).setUp()
Expand All @@ -240,6 +246,47 @@ def setUp(self):
cache=NonCache(),
)

# mock out for controllers UTs which use
# r2.lib.controllers as part of the flow.
self.autopatch(g.events, "queue_production", MockEventQueue())
self.autopatch(g.events, "queue_test", MockEventQueue())

self.simple_event = self.autopatch(g.stats, "simple_event")

self.user_agent = "Hacky McBrowser/1.0"
self.device_id = None

# Lastly, pull the app out of test mode so it'll load controllers on
# first use
RedditApp.test_mode = False

def do_post(self, action, params, headers=None, expect_errors=False):

assert self.CONTROLLER is not None

body = self.make_qs(**params)

headers = headers or {}
headers.setdefault('User-Agent', self.user_agent)
if self.device_id:
headers.setdefault('Client-Vendor-ID', self.device_id)
for k, v in self.additional_headers(headers, body).iteritems():
headers.setdefault(k, v)
headers = {k: v for k, v in headers.iteritems() if v is not None}
return self.app.post(
url_for(controller=self.CONTROLLER,
action=self.ACTIONS.get(action, action)),
extra_environ={"REMOTE_ADDR": "1.2.3.4"},
headers=headers,
params=body,
expect_errors=expect_errors,
)

def make_qs(self, **kw):
"""Convert the provided kw into a kw string suitable for app.post."""
return query_string(kw).lstrip("?")

def additional_headers(self, headers, body):
"""Additional generated headers to be added to the request.
"""
return {}
105 changes: 105 additions & 0 deletions r2/r2/tests/functional/controller/del_msg_test.py
@@ -0,0 +1,105 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of
# the Original Code is reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
# Inc. All Rights Reserved.
###############################################################################
import contextlib

from r2.tests import RedditControllerTestCase
from mock import patch, MagicMock
from r2.lib.validator import VByName, VUser, VModhash

from r2.models import Link, Message, Account

from pylons import app_globals as g


class DelMsgTest(RedditControllerTestCase):
CONTROLLER = "api"

def setUp(self):
super(DelMsgTest, self).setUp()

self.id = 1

def test_del_msg_success(self):
"""Del_msg succeeds: Returns 200 and sets del_on_recipient."""
message = MagicMock(spec=Message)
message.name = "msg_1"
message.to_id = self.id
message.del_on_recipient = False

with self.mock_del_msg(message):
res = self.do_del_msg(message.name)

self.assertEqual(res.status, 200)
self.assertTrue(message.del_on_recipient)

def test_del_msg_failure_with_link(self):
"""Del_msg fails: Returns 200 and does not set del_on_recipient."""
link = MagicMock(spec=Link)
link.del_on_recipient = False
link.name = "msg_2"

with self.mock_del_msg(link):
res = self.do_del_msg(link.name)

self.assertEqual(res.status, 200)
self.assertFalse(link.del_on_recipient)

def test_del_msg_failure_with_null_msg(self):
"""Del_msg fails: Returns 200 and does not set del_on_recipient."""
message = MagicMock(spec=Message)
message.name = "msg_3"
message.to_id = self.id
message.del_on_recipient = False

with self.mock_del_msg(message, False):
res = self.do_del_msg(message.name)

self.assertEqual(res.status, 200)
self.assertFalse(message.del_on_recipient)

def test_del_msg_failure_with_sender(self):
"""Del_msg fails: Returns 200 and does not set del_on_recipient."""
message = MagicMock(spec=Message)
message.name = "msg_3"
message.to_id = self.id + 1
message.del_on_recipient = False

with self.mock_del_msg(message):
res = self.do_del_msg(message.name)

self.assertEqual(res.status, 200)
self.assertFalse(message.del_on_recipient)

def mock_del_msg(self, thing, ret=True):
"""Context manager for mocking del_msg."""

return contextlib.nested(
patch.object(VByName, "run", return_value=thing if ret else None),
patch.object(VModhash, "run", side_effect=None),
patch.object(VUser, "run", side_effect=None),
patch.object(thing, "_commit", side_effect=None),
patch.object(Account, "_id", self.id, create=True),
patch.object(g.events, "message_event", side_effect=None),
)

def do_del_msg(self, name, **kw):
return self.do_post("del_msg", {"id": name}, **kw)
40 changes: 40 additions & 0 deletions r2/r2/tests/functional/controller/login/api_tests.py
@@ -0,0 +1,40 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of
# the Original Code is reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
# Inc. All Rights Reserved.
###############################################################################
from r2.tests import RedditControllerTestCase
from common import LoginRegBase


class LoginRegTests(LoginRegBase, RedditControllerTestCase):
CONTROLLER = "api"

def setUp(self):
RedditControllerTestCase.setUp(self)
LoginRegBase.setUp(self)

def assert_success(self, res):
self.assertEqual(res.status, 200)
self.assertTrue("error" not in res)

def assert_failure(self, res, code=None):
self.assertEqual(res.status, 200)
self.assertTrue("error" in res)
self.assertTrue(code in res)

0 comments on commit 7344336

Please sign in to comment.