Skip to content

Commit 952af19

Browse files
committed
Merge branch 'release/19.25.0'
2 parents 344f263 + 82dbb96 commit 952af19

37 files changed

+397
-621
lines changed

CHANGELOG

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.
44

5+
19.25.0 (2019-09-05)
6+
===================
7+
- Automate account deactivation if users have no content
8+
- Clean up EZID workflow
9+
- Check redirect URL's for spam
10+
511
19.24.0 (2019-08-27)
612
===================
713
- APIv2: Allow creating a node with a license attached on creation

addons/forward/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# -*- coding: utf-8 -*-
2+
from osf.utils.requests import get_request_and_user_id, get_headers_from_request
23

34
from addons.base.models import BaseNodeSettings
45
from dirtyfields import DirtyFieldsMixin
56
from django.db import models
67
from osf.exceptions import ValidationValueError
78
from osf.models.validators import validate_no_html
9+
from osf.models import OSFUser
810

911

1012
class NodeSettings(DirtyFieldsMixin, BaseNodeSettings):
@@ -33,6 +35,17 @@ def after_register(self, node, registration, user, save=True):
3335

3436
return clone, None
3537

38+
def save(self, request=None, *args, **kwargs):
39+
super(NodeSettings, self).save(*args, **kwargs)
40+
if request:
41+
if not hasattr(request, 'user'): # TODO: remove when Flask is removed
42+
_, user_id = get_request_and_user_id()
43+
user = OSFUser.load(user_id)
44+
else:
45+
user = request.user
46+
47+
self.owner.check_spam(user, {'addons_forward_node_settings__url'}, get_headers_from_request(request))
48+
3649
def clean(self):
3750
if self.url and self.owner._id in self.url:
3851
raise ValidationValueError('Circular URL')

addons/forward/tests/test_views.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
import mock
12
import pytest
23

34
from nose.tools import assert_equal
45

56
from addons.forward.tests.utils import ForwardAddonTestCase
67
from tests.base import OsfTestCase
8+
from website import settings
79

810
pytestmark = pytest.mark.django_db
911

10-
class TestForwardLogs(ForwardAddonTestCase, OsfTestCase):
12+
class TestForward(ForwardAddonTestCase, OsfTestCase):
1113

1214
def setUp(self):
13-
super(TestForwardLogs, self).setUp()
15+
super(TestForward, self).setUp()
1416
self.app.authenticate(*self.user.auth)
1517

1618
def test_change_url_log_added(self):
@@ -40,3 +42,19 @@ def test_change_timeout_log_not_added(self):
4042
self.project.logs.count(),
4143
log_count
4244
)
45+
46+
@mock.patch.object(settings, 'SPAM_CHECK_ENABLED', True)
47+
@mock.patch('osf.models.node.Node.do_check_spam')
48+
def test_change_url_check_spam(self, mock_check_spam):
49+
self.project.is_public = True
50+
self.project.save()
51+
self.app.put_json(self.project.api_url_for('forward_config_put'), {'url': 'http://possiblyspam.com'})
52+
53+
assert mock_check_spam.called
54+
data, _ = mock_check_spam.call_args
55+
author, author_email, content, request_headers = data
56+
57+
assert author == self.user.fullname
58+
assert author_email == self.user.username
59+
assert content == 'http://possiblyspam.com'
60+

addons/forward/views/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def forward_config_put(auth, node_addon, **kwargs):
4444
# Save settings and get changed fields; crash if validation fails
4545
try:
4646
dirty_fields = node_addon.get_dirty_fields()
47-
node_addon.save()
47+
node_addon.save(request=request)
4848
except ValidationValueError:
4949
raise HTTPError(http.BAD_REQUEST)
5050

api/addons/forward/test_views.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import mock
2+
import pytest
3+
4+
from addons.forward.tests.utils import ForwardAddonTestCase
5+
from tests.base import OsfTestCase
6+
from website import settings
7+
from tests.json_api_test_app import JSONAPITestApp
8+
9+
pytestmark = pytest.mark.django_db
10+
11+
class TestForward(ForwardAddonTestCase, OsfTestCase):
12+
"""
13+
Forward (the redirect url has two v2 routes, one is addon based `/v2/nodes/{}/addons/forward/` one is node settings
14+
based `/v2/nodes/{}/settings/` they both need to be checked for spam each time they are used to modify a redirect url.
15+
"""
16+
17+
django_app = JSONAPITestApp()
18+
19+
def setUp(self):
20+
super(TestForward, self).setUp()
21+
self.app.authenticate(*self.user.auth)
22+
23+
@mock.patch.object(settings, 'SPAM_CHECK_ENABLED', True)
24+
@mock.patch('osf.models.node.Node.do_check_spam')
25+
def test_change_url_check_spam(self, mock_check_spam):
26+
self.project.is_public = True
27+
self.project.save()
28+
self.django_app.put_json_api(
29+
'/v2/nodes/{}/addons/forward/'.format(self.project._id),
30+
{'data': {'attributes': {'url': 'http://possiblyspam.com'}}},
31+
auth=self.user.auth,
32+
)
33+
34+
assert mock_check_spam.called
35+
data, _ = mock_check_spam.call_args
36+
author, author_email, content, request_headers = data
37+
38+
assert author == self.user.fullname
39+
assert author_email == self.user.username
40+
assert content == 'http://possiblyspam.com'
41+
42+
@mock.patch.object(settings, 'SPAM_CHECK_ENABLED', True)
43+
@mock.patch('osf.models.node.Node.do_check_spam')
44+
def test_change_url_check_spam_node_settings(self, mock_check_spam):
45+
self.project.is_public = True
46+
self.project.save()
47+
48+
payload = {
49+
'data': {
50+
'type': 'node-settings',
51+
'attributes': {
52+
'access_requests_enabled': False,
53+
'redirect_link_url': 'http://possiblyspam.com',
54+
},
55+
},
56+
}
57+
58+
self.django_app.put_json_api(
59+
'/v2/nodes/{}/settings/'.format(self.project._id),
60+
payload,
61+
auth=self.user.auth,
62+
)
63+
64+
assert mock_check_spam.called
65+
data, _ = mock_check_spam.call_args
66+
author, author_email, content, request_headers = data
67+
68+
assert author == self.user.fullname
69+
assert author_email == self.user.username
70+
assert content == 'http://possiblyspam.com'
71+
72+
def test_invalid_url(self):
73+
res = self.django_app.put_json_api(
74+
'/v2/nodes/{}/addons/forward/'.format(self.project._id),
75+
{'data': {'attributes': {'url': 'bad url'}}},
76+
auth=self.user.auth, expect_errors=True,
77+
)
78+
assert res.status_code == 400
79+
error = res.json['errors'][0]
80+
81+
assert error['detail'] == 'Enter a valid URL.'

api/nodes/serializers.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ class Meta:
897897

898898
# Forward-specific
899899
label = ser.CharField(required=False, allow_blank=True)
900-
url = ser.CharField(required=False, allow_blank=True)
900+
url = ser.URLField(required=False, allow_blank=True)
901901

902902
links = LinksField({
903903
'self': 'get_absolute_url',
@@ -923,7 +923,9 @@ def create(self, validated_data):
923923
class ForwardNodeAddonSettingsSerializer(NodeAddonSettingsSerializerBase):
924924

925925
def update(self, instance, validated_data):
926-
auth = Auth(self.context['request'].user)
926+
request = self.context['request']
927+
user = request.user
928+
auth = Auth(user)
927929
set_url = 'url' in validated_data
928930
set_label = 'label' in validated_data
929931

@@ -953,7 +955,10 @@ def update(self, instance, validated_data):
953955
instance.label = label
954956
url_changed = True
955957

956-
instance.save()
958+
try:
959+
instance.save(request=request)
960+
except ValidationError as e:
961+
raise exceptions.ValidationError(detail=str(e))
957962

958963
if url_changed:
959964
# add log here because forward architecture isn't great
@@ -968,7 +973,6 @@ def update(self, instance, validated_data):
968973
auth=auth,
969974
save=True,
970975
)
971-
972976
return instance
973977

974978

@@ -1805,7 +1809,10 @@ def update_forward_fields(self, obj, validated_data, auth):
18051809
save_forward = True
18061810

18071811
if save_forward:
1808-
forward_addon.save()
1812+
try:
1813+
forward_addon.save(request=self.context['request'])
1814+
except ValidationError as e:
1815+
raise exceptions.ValidationError(detail=str(e))
18091816

18101817
def enable_or_disable_addon(self, obj, should_enable, addon_name, auth):
18111818
"""

api/users/serializers.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@
2020
from osf.exceptions import ValidationValueError, ValidationError, BlacklistedEmailError
2121
from osf.models import OSFUser, QuickFilesNode, Preprint
2222
from osf.utils.requests import string_type_request_headers
23-
from website.settings import MAILCHIMP_GENERAL_LIST, OSF_HELP_LIST, CONFIRM_REGISTRATIONS_BY_EMAIL, OSF_SUPPORT_EMAIL
23+
from website.settings import MAILCHIMP_GENERAL_LIST, OSF_HELP_LIST, CONFIRM_REGISTRATIONS_BY_EMAIL
2424
from osf.models.provider import AbstractProviderGroupObjectPermission
25-
from website import mails
2625
from website.profile.views import update_osf_help_mails_subscription, update_mailchimp_subscription
2726
from api.nodes.serializers import NodeSerializer, RegionRelationshipField
2827
from api.base.schemas.utils import validate_user_json, from_json
@@ -428,6 +427,7 @@ class UserSettingsSerializer(JSONAPISerializer):
428427
subscribe_osf_general_email = ser.SerializerMethodField()
429428
subscribe_osf_help_email = ser.SerializerMethodField()
430429
deactivation_requested = ser.BooleanField(source='requested_deactivation', required=False)
430+
contacted_deactivation = ser.BooleanField(required=False, read_only=True)
431431
secret = ser.SerializerMethodField(read_only=True)
432432

433433
def to_representation(self, instance):
@@ -526,18 +526,12 @@ def verify_two_factor(self, instance, value, two_factor_addon):
526526
two_factor_addon.save()
527527

528528
def request_deactivation(self, instance, requested_deactivation):
529+
529530
if instance.requested_deactivation != requested_deactivation:
530-
if requested_deactivation:
531-
mails.send_mail(
532-
to_addr=OSF_SUPPORT_EMAIL,
533-
mail=mails.REQUEST_DEACTIVATION,
534-
user=instance,
535-
can_change_preferences=False,
536-
)
537-
instance.email_last_sent = timezone.now()
538531
instance.requested_deactivation = requested_deactivation
532+
if not requested_deactivation:
533+
instance.contacted_deactivation = False
539534
instance.save()
540-
return
541535

542536
def to_representation(self, instance):
543537
"""

api_tests/nodes/views/test_node_detail.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,8 +1348,7 @@ def test_set_node_private_updates_doi(
13481348
assert res.status_code == 200
13491349
project_public.reload()
13501350
assert not project_public.is_public
1351-
mock_update_doi_metadata.assert_called_with(
1352-
project_public._id, status='unavailable')
1351+
mock_update_doi_metadata.assert_called_with(project_public._id)
13531352

13541353
@pytest.mark.enable_enqueue_task
13551354
@mock.patch('website.preprints.tasks.update_or_enqueue_on_preprint_updated')

api_tests/nodes/views/test_node_wiki_list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ def test_create_public_wiki_page(self, app, user_write_contributor, url_node_pub
338338
assert res.json['data']['attributes']['name'] == page_name
339339

340340
def test_create_public_wiki_page_with_content(self, app, user_write_contributor, url_node_public, project_public):
341-
page_name = fake.word()
341+
page_name = 'using random variables in tests can sometimes expose Testmon problems!'
342342
payload = create_wiki_payload(page_name)
343343
payload['data']['attributes']['content'] = 'my first wiki page'
344344
res = app.post_json_api(url_node_public, payload, auth=user_write_contributor.auth)

api_tests/users/views/test_user_settings_detail.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -250,31 +250,24 @@ def test_patch_requested_deactivation(self, mock_mail, app, user_one, user_two,
250250
assert res.status_code == 403
251251

252252
# Logged in, request to deactivate
253-
assert user_one.email_last_sent is None
254253
assert user_one.requested_deactivation is False
255254
res = app.patch_json_api(url, payload, auth=user_one.auth)
256255
assert res.status_code == 200
257256
user_one.reload()
258-
assert user_one.email_last_sent is not None
259257
assert user_one.requested_deactivation is True
260-
assert mock_mail.call_count == 1
261258

262259
# Logged in, deactivation already requested
263260
res = app.patch_json_api(url, payload, auth=user_one.auth)
264261
assert res.status_code == 200
265262
user_one.reload()
266-
assert user_one.email_last_sent is not None
267263
assert user_one.requested_deactivation is True
268-
assert mock_mail.call_count == 1
269264

270265
# Logged in, request to cancel deactivate request
271266
payload['data']['attributes']['deactivation_requested'] = False
272267
res = app.patch_json_api(url, payload, auth=user_one.auth)
273268
assert res.status_code == 200
274269
user_one.reload()
275-
assert user_one.email_last_sent is not None
276270
assert user_one.requested_deactivation is False
277-
assert mock_mail.call_count == 1
278271

279272
@mock.patch('framework.auth.views.mails.send_mail')
280273
def test_patch_invalid_type(self, mock_mail, app, user_one, url, payload):

0 commit comments

Comments
 (0)