Skip to content

Commit e019c3d

Browse files
committed
Merge branch 'release/19.24.0'
2 parents 5a7d802 + a5cbb4c commit e019c3d

31 files changed

+708
-104
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ language: python
44

55
python:
66
- "2.7"
7-
dist: trusty
7+
88
sudo: false
9+
dist: trusty
910

1011
# TODO: uncomment when https://github.com/travis-ci/travis-ci/issues/8836 is resolved
1112
# addons:

CHANGELOG

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

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

5+
19.24.0 (2019-08-27)
6+
===================
7+
- APIv2: Allow creating a node with a license attached on creation
8+
- APIv2: Prevent creating a node link to the node itself, a parent, or a child
9+
- APIv2: Have the move/copy WB hooks update storage usage
10+
- Exclude collection groups from user page in Django's admin app
11+
- Have osfstorage move/copy hooks recursively update the latest file version's region
12+
- Fix password reset tokens
13+
- Do not reveal entire google drive file path in node logs
14+
- Allow logging in from password reset and forgot password pages
15+
- Fix broken social links on add contributors modal
16+
- Fix email typos in embargoed registration emails
17+
- Improve registration and retraction node log display text
18+
- Fix logs if cron job automatically approves a withdrawal
19+
520
19.23.0 (2019-08-19)
621
===================
722
- Represents scopes as an m2m field on personal access tokens instead of a CharField

addons/osfstorage/models.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,16 +166,16 @@ def delete(self, user=None, parent=None, **kwargs):
166166
self._materialized_path = self.materialized_path
167167
return super(OsfStorageFileNode, self).delete(user=user, parent=parent) if self._check_delete_allowed() else None
168168

169+
def update_region_from_latest_version(self, destination_parent):
170+
raise NotImplementedError
171+
169172
def move_under(self, destination_parent, name=None):
170173
if self.is_preprint_primary:
171174
if self.target != destination_parent.target or self.provider != destination_parent.provider:
172175
raise exceptions.FileNodeIsPrimaryFile()
173176
if self.is_checked_out:
174177
raise exceptions.FileNodeCheckedOutError()
175-
most_recent_fileversion = self.versions.select_related('region').order_by('-created').first()
176-
if most_recent_fileversion and most_recent_fileversion.region != destination_parent.target.osfstorage_region:
177-
most_recent_fileversion.region = destination_parent.target.osfstorage_region
178-
most_recent_fileversion.save()
178+
self.update_region_from_latest_version(destination_parent)
179179
return super(OsfStorageFileNode, self).move_under(destination_parent, name)
180180

181181
def check_in_or_out(self, user, checkout, save=False):
@@ -293,6 +293,12 @@ def serialize(self, include_full=None, version=None):
293293
})
294294
return ret
295295

296+
def update_region_from_latest_version(self, destination_parent):
297+
most_recent_fileversion = self.versions.select_related('region').order_by('-created').first()
298+
if most_recent_fileversion and most_recent_fileversion.region != destination_parent.target.osfstorage_region:
299+
most_recent_fileversion.region = destination_parent.target.osfstorage_region
300+
most_recent_fileversion.save()
301+
296302
def create_version(self, creator, location, metadata=None):
297303
latest_version = self.get_version()
298304
version = FileVersion(identifier=self.versions.count() + 1, creator=creator, location=location)
@@ -451,6 +457,9 @@ def serialize(self, include_full=False, version=None):
451457
ret['fullPath'] = self.materialized_path
452458
return ret
453459

460+
def update_region_from_latest_version(self, destination_parent):
461+
for child in self.children.all().prefetch_related('versions'):
462+
child.update_region_from_latest_version(destination_parent)
454463

455464
class Region(models.Model):
456465
_id = models.CharField(max_length=255, db_index=True)

addons/osfstorage/tests/test_models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,30 @@ def test_move_nested(self):
334334
assert_equal(new_project, move_to.target)
335335
assert_equal(new_project, child.target)
336336

337+
def test_move_nested_between_regions(self):
338+
canada = RegionFactory()
339+
new_component = NodeFactory(parent=self.project)
340+
component_node_settings = new_component.get_addon('osfstorage')
341+
component_node_settings.region = canada
342+
component_node_settings.save()
343+
344+
move_to = component_node_settings.get_root()
345+
to_move = self.node_settings.get_root().append_folder('Aaah').append_folder('Woop')
346+
child = to_move.append_file('There it is')
347+
348+
for _ in range(2):
349+
version = factories.FileVersionFactory(region=self.node_settings.region)
350+
child.versions.add(version)
351+
child.save()
352+
353+
moved = to_move.move_under(move_to)
354+
child.reload()
355+
356+
assert new_component == child.target
357+
versions = child.versions.order_by('-created')
358+
assert versions.first().region == component_node_settings.region
359+
assert versions.last().region == self.node_settings.region
360+
337361
def test_copy_rename(self):
338362
to_copy = self.node_settings.get_root().append_file('Carp')
339363
copy_to = self.node_settings.get_root().append_folder('Cloud')

addons/wiki/tests/test_wiki.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1238,7 +1238,6 @@ def test_serialize_wiki_settings(self):
12381238
node = NodeFactory(parent=self.project, creator=self.user, is_public=True)
12391239
node.get_addon('wiki').set_editing(
12401240
permissions=True, auth=self.consolidate_auth, log=True)
1241-
node.add_pointer(self.project, Auth(self.user))
12421241
node.save()
12431242
data = serialize_wiki_settings(self.user, [node])
12441243
expected = [{

admin_tests/base/test_utils.py

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
from nose.tools import * # noqa: F403
22
import datetime as datetime
3+
import pytest
34

5+
from django.test import RequestFactory
46
from django.db.models import Q
57
from django.contrib.auth.models import Group
68
from django.core.exceptions import ValidationError, PermissionDenied
9+
from django.contrib.admin.sites import AdminSite
10+
from django.forms.models import model_to_dict
11+
from django.http import QueryDict
12+
713

814
from tests.base import AdminTestCase
915

10-
from osf_tests.factories import SubjectFactory, UserFactory, RegistrationFactory
16+
from osf_tests.factories import SubjectFactory, UserFactory, RegistrationFactory, PreprintFactory
1117

12-
from osf.models import Subject
18+
from osf.models import Subject, OSFUser, Collection
1319
from osf.models.provider import rules_to_subjects
1420
from admin.base.utils import get_subject_rules, change_embargo_date
21+
from osf.admin import OSFUserAdmin
1522

1623

1724
import logging
1825
logger = logging.getLogger(__name__)
1926
logging.basicConfig(level=logging.INFO)
27+
pytestmark = pytest.mark.django_db
2028

2129

2230
class TestSubjectRules(AdminTestCase):
@@ -158,3 +166,82 @@ def test_change_embargo_date(self):
158166
assert_almost_equal(self.registration.embargo.end_date, self.date_valid2, delta=datetime.timedelta(days=1))
159167

160168
# Add a test to check privatizing
169+
170+
site = AdminSite()
171+
172+
class TestGroupCollectionsPreprints:
173+
@pytest.mark.enable_bookmark_creation
174+
@pytest.fixture()
175+
def user(self):
176+
return UserFactory()
177+
178+
@pytest.fixture()
179+
def admin_url(self, user):
180+
return '/admin/osf/osfuser/{}/change/'.format(user.id)
181+
182+
@pytest.fixture()
183+
def preprint(self, user):
184+
return PreprintFactory(creator=user)
185+
186+
@pytest.fixture()
187+
def get_request(self, admin_url, user):
188+
request = RequestFactory().get(admin_url)
189+
request.user = user
190+
return request
191+
192+
@pytest.fixture()
193+
def post_request(self, admin_url, user):
194+
request = RequestFactory().post(admin_url)
195+
request.user = user
196+
return request
197+
198+
@pytest.fixture()
199+
def osf_user_admin(self):
200+
return OSFUserAdmin(OSFUser, site)
201+
202+
@pytest.mark.enable_bookmark_creation
203+
def test_admin_app_formfield_collections(self, preprint, user, get_request, osf_user_admin):
204+
""" Testing OSFUserAdmin.formfield_many_to_many.
205+
This should not return any bookmark collections or preprint groups, even if the user is a member.
206+
"""
207+
208+
formfield = (osf_user_admin.formfield_for_manytomany(OSFUser.groups.field, request=get_request))
209+
queryset = formfield.queryset
210+
211+
collections_group = Collection.objects.filter(creator=user, is_bookmark_collection=True)[0].get_group('admin')
212+
assert(collections_group not in queryset)
213+
214+
assert(preprint.get_group('admin') not in queryset)
215+
216+
@pytest.mark.enable_bookmark_creation
217+
def test_admin_app_save_related_collections(self, post_request, osf_user_admin, user, preprint):
218+
""" Testing OSFUserAdmin.save_related
219+
This should maintain the bookmark collections and preprint groups the user is a member of
220+
even though they aren't explicitly returned by the form.
221+
"""
222+
223+
form = osf_user_admin.get_form(request=post_request, obj=user)
224+
data_dict = model_to_dict(user)
225+
post_form = form(data_dict, instance=user)
226+
227+
# post_form.errors.keys() generates a list of fields causing JSON Related errors
228+
# which are preventing the form from being valid (which is required for the form to be saved).
229+
# By setting the field to '{}', this makes the form valid and resolves JSON errors.
230+
231+
for field in post_form.errors.keys():
232+
if field == 'groups':
233+
data_dict['groups'] = []
234+
else:
235+
data_dict[field] = '{}'
236+
post_form = form(data_dict, instance=user)
237+
assert(post_form.is_valid())
238+
post_form.save(commit=False)
239+
qdict = QueryDict('', mutable=True)
240+
qdict.update(data_dict)
241+
post_request.POST = qdict
242+
osf_user_admin.save_related(request=post_request, form=post_form, formsets=[], change=True)
243+
244+
collections_group = Collection.objects.filter(creator=user, is_bookmark_collection=True)[0].get_group('admin')
245+
assert(collections_group in user.groups.all())
246+
247+
assert(preprint.get_group('admin') in user.groups.all())

api/base/serializers.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,7 +1674,12 @@ def update(self, instance, validated_data):
16741674
for pointer in remove:
16751675
collection.rm_pointer(pointer, auth)
16761676
for node in add:
1677-
collection.add_pointer(node, auth)
1677+
try:
1678+
collection.add_pointer(node, auth)
1679+
except ValueError as e:
1680+
raise api_exceptions.InvalidModelValueError(
1681+
detail=str(e),
1682+
)
16781683

16791684
return self.make_instance_obj(collection)
16801685

@@ -1689,8 +1694,12 @@ def create(self, validated_data):
16891694
raise api_exceptions.RelationshipPostMakesNoChanges
16901695

16911696
for node in add:
1692-
collection.add_pointer(node, auth)
1693-
1697+
try:
1698+
collection.add_pointer(node, auth)
1699+
except ValueError as e:
1700+
raise api_exceptions.InvalidModelValueError(
1701+
detail=str(e),
1702+
)
16941703
return self.make_instance_obj(collection)
16951704

16961705

@@ -1747,7 +1756,12 @@ def update(self, instance, validated_data):
17471756
for pointer in remove:
17481757
collection.rm_pointer(pointer, auth)
17491758
for node in add:
1750-
collection.add_pointer(node, auth)
1759+
try:
1760+
collection.add_pointer(node, auth)
1761+
except ValueError as e:
1762+
raise api_exceptions.InvalidModelValueError(
1763+
detail=str(e),
1764+
)
17511765

17521766
return self.make_instance_obj(collection)
17531767

@@ -1762,7 +1776,12 @@ def create(self, validated_data):
17621776
raise api_exceptions.RelationshipPostMakesNoChanges
17631777

17641778
for node in add:
1765-
collection.add_pointer(node, auth)
1779+
try:
1780+
collection.add_pointer(node, auth)
1781+
except ValueError as e:
1782+
raise api_exceptions.InvalidModelValueError(
1783+
detail=str(e),
1784+
)
17661785

17671786
return self.make_instance_obj(collection)
17681787

api/nodes/serializers.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ def to_internal_value(self, data):
141141

142142
class NodeLicenseSerializer(BaseAPISerializer):
143143

144-
copyright_holders = ser.ListField(allow_empty=True)
145-
year = ser.CharField(allow_blank=True)
144+
copyright_holders = ser.ListField(allow_empty=True, required=False)
145+
year = ser.CharField(allow_blank=True, required=False)
146146

147147
class Meta:
148148
type_ = 'node_licenses'
@@ -206,8 +206,12 @@ class Meta:
206206
type_ = 'styled-citations'
207207

208208
def get_license_details(node, validated_data):
209-
license = node.license if isinstance(node, Preprint) else node.node_license
210-
209+
if node:
210+
license = node.license if isinstance(node, Preprint) else node.node_license
211+
else:
212+
license = None
213+
if ('license_type' not in validated_data and not (license and license.node_license.license_id)):
214+
raise exceptions.ValidationError(detail='License ID must be provided for a Node License.')
211215
license_id = license.node_license.license_id if license else None
212216
license_year = license.year if license else None
213217
license_holders = license.copyright_holders if license else []
@@ -747,10 +751,18 @@ def create(self, validated_data):
747751
tag_instances = []
748752
affiliated_institutions = None
749753
region_id = None
754+
license_details = None
750755
if 'affiliated_institutions' in validated_data:
751756
affiliated_institutions = validated_data.pop('affiliated_institutions')
752757
if 'region_id' in validated_data:
753758
region_id = validated_data.pop('region_id')
759+
if 'license_type' in validated_data or 'license' in validated_data:
760+
try:
761+
license_details = get_license_details(None, validated_data)
762+
except ValidationError as e:
763+
raise InvalidModelValueError(detail=str(e.messages[0]))
764+
validated_data.pop('license', None)
765+
validated_data.pop('license_type', None)
754766
if 'tags' in validated_data:
755767
tags = validated_data.pop('tags')
756768
for tag in tags:
@@ -806,6 +818,20 @@ def create(self, validated_data):
806818
node.subjects.add(parent.subjects.all())
807819
node.save()
808820

821+
if license_details:
822+
try:
823+
node.set_node_license(
824+
{
825+
'id': license_details.get('id') if license_details.get('id') else 'NONE',
826+
'year': license_details.get('year'),
827+
'copyrightHolders': license_details.get('copyrightHolders') or license_details.get('copyright_holders', []),
828+
},
829+
auth=get_user_auth(request),
830+
save=True,
831+
)
832+
except ValidationError as e:
833+
raise InvalidModelValueError(detail=str(e.message))
834+
809835
if not region_id:
810836
region_id = self.context.get('region_id')
811837
if region_id:
@@ -1314,10 +1340,10 @@ def create(self, validated_data):
13141340
try:
13151341
pointer = node.add_pointer(pointer_node, auth, save=True)
13161342
return pointer
1317-
except ValueError:
1343+
except ValueError as e:
13181344
raise InvalidModelValueError(
13191345
source={'pointer': '/data/relationships/node_links/data/id'},
1320-
detail='Target Node \'{}\' already pointed to by \'{}\'.'.format(target_node_id, node._id),
1346+
detail=str(e),
13211347
)
13221348

13231349
def update(self, instance, validated_data):

0 commit comments

Comments
 (0)