diff --git a/.travis.yml b/.travis.yml
index 9c6d46cb7dd..ef28085b46f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,8 +4,9 @@ language: python
python:
- "2.7"
-dist: trusty
+
sudo: false
+dist: trusty
# TODO: uncomment when https://github.com/travis-ci/travis-ci/issues/8836 is resolved
# addons:
diff --git a/CHANGELOG b/CHANGELOG
index da08e0a854d..791c594bb10 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,6 +2,21 @@
We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.
+19.24.0 (2019-08-27)
+===================
+- APIv2: Allow creating a node with a license attached on creation
+- APIv2: Prevent creating a node link to the node itself, a parent, or a child
+- APIv2: Have the move/copy WB hooks update storage usage
+- Exclude collection groups from user page in Django's admin app
+- Have osfstorage move/copy hooks recursively update the latest file version's region
+- Fix password reset tokens
+- Do not reveal entire google drive file path in node logs
+- Allow logging in from password reset and forgot password pages
+- Fix broken social links on add contributors modal
+- Fix email typos in embargoed registration emails
+- Improve registration and retraction node log display text
+- Fix logs if cron job automatically approves a withdrawal
+
19.23.0 (2019-08-19)
===================
- Represents scopes as an m2m field on personal access tokens instead of a CharField
diff --git a/addons/osfstorage/models.py b/addons/osfstorage/models.py
index 54703208bee..43e521ed186 100644
--- a/addons/osfstorage/models.py
+++ b/addons/osfstorage/models.py
@@ -166,16 +166,16 @@ def delete(self, user=None, parent=None, **kwargs):
self._materialized_path = self.materialized_path
return super(OsfStorageFileNode, self).delete(user=user, parent=parent) if self._check_delete_allowed() else None
+ def update_region_from_latest_version(self, destination_parent):
+ raise NotImplementedError
+
def move_under(self, destination_parent, name=None):
if self.is_preprint_primary:
if self.target != destination_parent.target or self.provider != destination_parent.provider:
raise exceptions.FileNodeIsPrimaryFile()
if self.is_checked_out:
raise exceptions.FileNodeCheckedOutError()
- most_recent_fileversion = self.versions.select_related('region').order_by('-created').first()
- if most_recent_fileversion and most_recent_fileversion.region != destination_parent.target.osfstorage_region:
- most_recent_fileversion.region = destination_parent.target.osfstorage_region
- most_recent_fileversion.save()
+ self.update_region_from_latest_version(destination_parent)
return super(OsfStorageFileNode, self).move_under(destination_parent, name)
def check_in_or_out(self, user, checkout, save=False):
@@ -293,6 +293,12 @@ def serialize(self, include_full=None, version=None):
})
return ret
+ def update_region_from_latest_version(self, destination_parent):
+ most_recent_fileversion = self.versions.select_related('region').order_by('-created').first()
+ if most_recent_fileversion and most_recent_fileversion.region != destination_parent.target.osfstorage_region:
+ most_recent_fileversion.region = destination_parent.target.osfstorage_region
+ most_recent_fileversion.save()
+
def create_version(self, creator, location, metadata=None):
latest_version = self.get_version()
version = FileVersion(identifier=self.versions.count() + 1, creator=creator, location=location)
@@ -451,6 +457,9 @@ def serialize(self, include_full=False, version=None):
ret['fullPath'] = self.materialized_path
return ret
+ def update_region_from_latest_version(self, destination_parent):
+ for child in self.children.all().prefetch_related('versions'):
+ child.update_region_from_latest_version(destination_parent)
class Region(models.Model):
_id = models.CharField(max_length=255, db_index=True)
diff --git a/addons/osfstorage/tests/test_models.py b/addons/osfstorage/tests/test_models.py
index ff973e7801c..4e7fb592aed 100644
--- a/addons/osfstorage/tests/test_models.py
+++ b/addons/osfstorage/tests/test_models.py
@@ -334,6 +334,30 @@ def test_move_nested(self):
assert_equal(new_project, move_to.target)
assert_equal(new_project, child.target)
+ def test_move_nested_between_regions(self):
+ canada = RegionFactory()
+ new_component = NodeFactory(parent=self.project)
+ component_node_settings = new_component.get_addon('osfstorage')
+ component_node_settings.region = canada
+ component_node_settings.save()
+
+ move_to = component_node_settings.get_root()
+ to_move = self.node_settings.get_root().append_folder('Aaah').append_folder('Woop')
+ child = to_move.append_file('There it is')
+
+ for _ in range(2):
+ version = factories.FileVersionFactory(region=self.node_settings.region)
+ child.versions.add(version)
+ child.save()
+
+ moved = to_move.move_under(move_to)
+ child.reload()
+
+ assert new_component == child.target
+ versions = child.versions.order_by('-created')
+ assert versions.first().region == component_node_settings.region
+ assert versions.last().region == self.node_settings.region
+
def test_copy_rename(self):
to_copy = self.node_settings.get_root().append_file('Carp')
copy_to = self.node_settings.get_root().append_folder('Cloud')
diff --git a/addons/wiki/tests/test_wiki.py b/addons/wiki/tests/test_wiki.py
index bfcd65fc5ff..a60b5aaa03d 100644
--- a/addons/wiki/tests/test_wiki.py
+++ b/addons/wiki/tests/test_wiki.py
@@ -1238,7 +1238,6 @@ def test_serialize_wiki_settings(self):
node = NodeFactory(parent=self.project, creator=self.user, is_public=True)
node.get_addon('wiki').set_editing(
permissions=True, auth=self.consolidate_auth, log=True)
- node.add_pointer(self.project, Auth(self.user))
node.save()
data = serialize_wiki_settings(self.user, [node])
expected = [{
diff --git a/admin_tests/base/test_utils.py b/admin_tests/base/test_utils.py
index 89b8b843c33..3506c57534c 100644
--- a/admin_tests/base/test_utils.py
+++ b/admin_tests/base/test_utils.py
@@ -1,22 +1,30 @@
from nose.tools import * # noqa: F403
import datetime as datetime
+import pytest
+from django.test import RequestFactory
from django.db.models import Q
from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError, PermissionDenied
+from django.contrib.admin.sites import AdminSite
+from django.forms.models import model_to_dict
+from django.http import QueryDict
+
from tests.base import AdminTestCase
-from osf_tests.factories import SubjectFactory, UserFactory, RegistrationFactory
+from osf_tests.factories import SubjectFactory, UserFactory, RegistrationFactory, PreprintFactory
-from osf.models import Subject
+from osf.models import Subject, OSFUser, Collection
from osf.models.provider import rules_to_subjects
from admin.base.utils import get_subject_rules, change_embargo_date
+from osf.admin import OSFUserAdmin
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
+pytestmark = pytest.mark.django_db
class TestSubjectRules(AdminTestCase):
@@ -158,3 +166,82 @@ def test_change_embargo_date(self):
assert_almost_equal(self.registration.embargo.end_date, self.date_valid2, delta=datetime.timedelta(days=1))
# Add a test to check privatizing
+
+site = AdminSite()
+
+class TestGroupCollectionsPreprints:
+ @pytest.mark.enable_bookmark_creation
+ @pytest.fixture()
+ def user(self):
+ return UserFactory()
+
+ @pytest.fixture()
+ def admin_url(self, user):
+ return '/admin/osf/osfuser/{}/change/'.format(user.id)
+
+ @pytest.fixture()
+ def preprint(self, user):
+ return PreprintFactory(creator=user)
+
+ @pytest.fixture()
+ def get_request(self, admin_url, user):
+ request = RequestFactory().get(admin_url)
+ request.user = user
+ return request
+
+ @pytest.fixture()
+ def post_request(self, admin_url, user):
+ request = RequestFactory().post(admin_url)
+ request.user = user
+ return request
+
+ @pytest.fixture()
+ def osf_user_admin(self):
+ return OSFUserAdmin(OSFUser, site)
+
+ @pytest.mark.enable_bookmark_creation
+ def test_admin_app_formfield_collections(self, preprint, user, get_request, osf_user_admin):
+ """ Testing OSFUserAdmin.formfield_many_to_many.
+ This should not return any bookmark collections or preprint groups, even if the user is a member.
+ """
+
+ formfield = (osf_user_admin.formfield_for_manytomany(OSFUser.groups.field, request=get_request))
+ queryset = formfield.queryset
+
+ collections_group = Collection.objects.filter(creator=user, is_bookmark_collection=True)[0].get_group('admin')
+ assert(collections_group not in queryset)
+
+ assert(preprint.get_group('admin') not in queryset)
+
+ @pytest.mark.enable_bookmark_creation
+ def test_admin_app_save_related_collections(self, post_request, osf_user_admin, user, preprint):
+ """ Testing OSFUserAdmin.save_related
+ This should maintain the bookmark collections and preprint groups the user is a member of
+ even though they aren't explicitly returned by the form.
+ """
+
+ form = osf_user_admin.get_form(request=post_request, obj=user)
+ data_dict = model_to_dict(user)
+ post_form = form(data_dict, instance=user)
+
+ # post_form.errors.keys() generates a list of fields causing JSON Related errors
+ # which are preventing the form from being valid (which is required for the form to be saved).
+ # By setting the field to '{}', this makes the form valid and resolves JSON errors.
+
+ for field in post_form.errors.keys():
+ if field == 'groups':
+ data_dict['groups'] = []
+ else:
+ data_dict[field] = '{}'
+ post_form = form(data_dict, instance=user)
+ assert(post_form.is_valid())
+ post_form.save(commit=False)
+ qdict = QueryDict('', mutable=True)
+ qdict.update(data_dict)
+ post_request.POST = qdict
+ osf_user_admin.save_related(request=post_request, form=post_form, formsets=[], change=True)
+
+ collections_group = Collection.objects.filter(creator=user, is_bookmark_collection=True)[0].get_group('admin')
+ assert(collections_group in user.groups.all())
+
+ assert(preprint.get_group('admin') in user.groups.all())
diff --git a/api/base/serializers.py b/api/base/serializers.py
index de74444fe1c..72386985968 100644
--- a/api/base/serializers.py
+++ b/api/base/serializers.py
@@ -1674,7 +1674,12 @@ def update(self, instance, validated_data):
for pointer in remove:
collection.rm_pointer(pointer, auth)
for node in add:
- collection.add_pointer(node, auth)
+ try:
+ collection.add_pointer(node, auth)
+ except ValueError as e:
+ raise api_exceptions.InvalidModelValueError(
+ detail=str(e),
+ )
return self.make_instance_obj(collection)
@@ -1689,8 +1694,12 @@ def create(self, validated_data):
raise api_exceptions.RelationshipPostMakesNoChanges
for node in add:
- collection.add_pointer(node, auth)
-
+ try:
+ collection.add_pointer(node, auth)
+ except ValueError as e:
+ raise api_exceptions.InvalidModelValueError(
+ detail=str(e),
+ )
return self.make_instance_obj(collection)
@@ -1747,7 +1756,12 @@ def update(self, instance, validated_data):
for pointer in remove:
collection.rm_pointer(pointer, auth)
for node in add:
- collection.add_pointer(node, auth)
+ try:
+ collection.add_pointer(node, auth)
+ except ValueError as e:
+ raise api_exceptions.InvalidModelValueError(
+ detail=str(e),
+ )
return self.make_instance_obj(collection)
@@ -1762,7 +1776,12 @@ def create(self, validated_data):
raise api_exceptions.RelationshipPostMakesNoChanges
for node in add:
- collection.add_pointer(node, auth)
+ try:
+ collection.add_pointer(node, auth)
+ except ValueError as e:
+ raise api_exceptions.InvalidModelValueError(
+ detail=str(e),
+ )
return self.make_instance_obj(collection)
diff --git a/api/nodes/serializers.py b/api/nodes/serializers.py
index d928cb71d79..fb585f31263 100644
--- a/api/nodes/serializers.py
+++ b/api/nodes/serializers.py
@@ -141,8 +141,8 @@ def to_internal_value(self, data):
class NodeLicenseSerializer(BaseAPISerializer):
- copyright_holders = ser.ListField(allow_empty=True)
- year = ser.CharField(allow_blank=True)
+ copyright_holders = ser.ListField(allow_empty=True, required=False)
+ year = ser.CharField(allow_blank=True, required=False)
class Meta:
type_ = 'node_licenses'
@@ -206,8 +206,12 @@ class Meta:
type_ = 'styled-citations'
def get_license_details(node, validated_data):
- license = node.license if isinstance(node, Preprint) else node.node_license
-
+ if node:
+ license = node.license if isinstance(node, Preprint) else node.node_license
+ else:
+ license = None
+ if ('license_type' not in validated_data and not (license and license.node_license.license_id)):
+ raise exceptions.ValidationError(detail='License ID must be provided for a Node License.')
license_id = license.node_license.license_id if license else None
license_year = license.year if license else None
license_holders = license.copyright_holders if license else []
@@ -747,10 +751,18 @@ def create(self, validated_data):
tag_instances = []
affiliated_institutions = None
region_id = None
+ license_details = None
if 'affiliated_institutions' in validated_data:
affiliated_institutions = validated_data.pop('affiliated_institutions')
if 'region_id' in validated_data:
region_id = validated_data.pop('region_id')
+ if 'license_type' in validated_data or 'license' in validated_data:
+ try:
+ license_details = get_license_details(None, validated_data)
+ except ValidationError as e:
+ raise InvalidModelValueError(detail=str(e.messages[0]))
+ validated_data.pop('license', None)
+ validated_data.pop('license_type', None)
if 'tags' in validated_data:
tags = validated_data.pop('tags')
for tag in tags:
@@ -806,6 +818,20 @@ def create(self, validated_data):
node.subjects.add(parent.subjects.all())
node.save()
+ if license_details:
+ try:
+ node.set_node_license(
+ {
+ 'id': license_details.get('id') if license_details.get('id') else 'NONE',
+ 'year': license_details.get('year'),
+ 'copyrightHolders': license_details.get('copyrightHolders') or license_details.get('copyright_holders', []),
+ },
+ auth=get_user_auth(request),
+ save=True,
+ )
+ except ValidationError as e:
+ raise InvalidModelValueError(detail=str(e.message))
+
if not region_id:
region_id = self.context.get('region_id')
if region_id:
@@ -1314,10 +1340,10 @@ def create(self, validated_data):
try:
pointer = node.add_pointer(pointer_node, auth, save=True)
return pointer
- except ValueError:
+ except ValueError as e:
raise InvalidModelValueError(
source={'pointer': '/data/relationships/node_links/data/id'},
- detail='Target Node \'{}\' already pointed to by \'{}\'.'.format(target_node_id, node._id),
+ detail=str(e),
)
def update(self, instance, validated_data):
diff --git a/api/wb/views.py b/api/wb/views.py
index 7b1300c2bcc..f8bb0387ea7 100644
--- a/api/wb/views.py
+++ b/api/wb/views.py
@@ -10,6 +10,9 @@
WaterbutlerMetadataSerializer,
)
+from api.caching.tasks import update_storage_usage
+
+
class FileMetadataView(APIView):
"""
Mixin with common code for WB move/copy hooks
@@ -77,7 +80,15 @@ def post(self, request, *args, **kwargs):
return response
def perform_file_action(self, source, destination, name):
- return source.move_under(destination, name)
+ dest_target = destination.target
+ source_target = source.target
+ ret = source.move_under(destination, name)
+
+ if dest_target != source_target:
+ update_storage_usage(source.target)
+ update_storage_usage(destination.target)
+
+ return ret
class CopyFileMetadataView(FileMetadataView):
@@ -89,4 +100,6 @@ class CopyFileMetadataView(FileMetadataView):
view_name = 'metadata-copy'
def perform_file_action(self, source, destination, name):
- return source.copy_under(destination, name)
+ ret = source.copy_under(destination, name)
+ update_storage_usage(destination.target)
+ return ret
diff --git a/api_tests/nodes/views/test_node_detail.py b/api_tests/nodes/views/test_node_detail.py
index dc5e128ba18..6c3f286ba67 100644
--- a/api_tests/nodes/views/test_node_detail.py
+++ b/api_tests/nodes/views/test_node_detail.py
@@ -2388,6 +2388,21 @@ def test_update_node_license_without_required_year_in_payload(
assert res.status_code == 400
assert res.json['errors'][0]['detail'] == 'year must be specified for this license'
+ def test_update_node_license_without_license_id(
+ self, node, make_payload, make_request, url_node, user_admin_contrib):
+ data = make_payload(
+ node_id=node._id,
+ license_year='2015',
+ copyright_holders=['Ben, Jerry']
+ )
+
+ res = make_request(
+ url_node, data,
+ auth=user_admin_contrib.auth,
+ expect_errors=True)
+ assert res.status_code == 400
+ assert res.json['errors'][0]['detail'] == 'License ID must be provided for a Node License.'
+
def test_update_node_license_without_required_copyright_holders_in_payload_(
self, user_admin_contrib, node, make_payload, make_request, license_no, url_node):
data = make_payload(
diff --git a/api_tests/nodes/views/test_node_linked_nodes.py b/api_tests/nodes/views/test_node_linked_nodes.py
index 7f0aca609a3..18f1a7208f4 100644
--- a/api_tests/nodes/views/test_node_linked_nodes.py
+++ b/api_tests/nodes/views/test_node_linked_nodes.py
@@ -6,6 +6,7 @@
NodeFactory,
OSFGroupFactory,
AuthUserFactory,
+ NodeRelationFactory,
)
from osf.utils.permissions import WRITE, READ
from website.project.signals import contributor_removed
@@ -359,7 +360,7 @@ def test_delete_invalid_payload(
def test_node_errors(
self, app, user, node_private, node_contrib,
node_public, node_other, make_payload,
- url_private, url_public):
+ url_private, url_public, node_linking_private):
# test_node_doesnt_exist
res = app.post_json_api(
@@ -453,6 +454,8 @@ def test_node_errors(
auth=user.auth, expect_errors=True
)
+ # test_put_child_node
+
assert res.status_code == 403
# test_delete_public_nodes_relationships_logged_out
@@ -471,6 +474,106 @@ def test_node_errors(
assert res.status_code == 403
+ # test_node_child_cannot_be_linked_on_create
+ node_child = NodeFactory(creator=user)
+ node_parent = NodeFactory(creator=user)
+ node_parent_child = NodeRelationFactory(child=node_child, parent=node_parent)
+ url = '/{}nodes/{}/relationships/linked_nodes/'.format(
+ API_BASE, node_parent_child.parent._id
+ )
+ res = app.post_json_api(
+ url, {
+ 'data': [{
+ 'type': 'linked_nodes',
+ 'id': node_parent_child.child._id
+ }]
+ },
+ auth=user.auth, expect_errors=True
+ )
+
+ assert res.status_code == 400
+
+ # test_linking_node_to_itself _on_create
+ node_self = NodeFactory(creator=user)
+ url = '/{}nodes/{}/relationships/linked_nodes/'.format(
+ API_BASE, node_self._id
+ )
+ res = app.post_json_api(
+ url_private, make_payload([node_linking_private._id]),
+ auth=user.auth, expect_errors=True
+ )
+
+ assert res.status_code == 400
+
+ # test_linking_child_node_to_parent_on_create
+ node_child = NodeFactory(creator=user)
+ node_parent = NodeFactory(creator=user)
+ node_parent_child = NodeRelationFactory(child=node_child, parent=node_parent)
+ url = '/{}nodes/{}/relationships/linked_nodes/'.format(
+ API_BASE, node_parent_child.child._id
+ )
+ res = app.post_json_api(
+ url, {
+ 'data': [{
+ 'type': 'linked_nodes',
+ 'id': node_parent_child.parent._id
+ }]
+ },
+ auth=user.auth, expect_errors=True
+ )
+
+ assert res.status_code == 400
+
+ # test_node_child_cannot_be_linked_on_update
+ node_child = NodeFactory(creator=user)
+ node_parent = NodeFactory(creator=user)
+ node_parent_child = NodeRelationFactory(child=node_child, parent=node_parent)
+ url = '/{}nodes/{}/relationships/linked_nodes/'.format(
+ API_BASE, node_parent_child.parent._id
+ )
+ res = app.put_json_api(
+ url, {
+ 'data': [{
+ 'type': 'linked_nodes',
+ 'id': node_parent_child.child._id
+ }]
+ },
+ auth=user.auth, expect_errors=True
+ )
+
+ assert res.status_code == 400
+
+ # test_linking_child_node_to_parent_on_update
+ node_child = NodeFactory(creator=user)
+ node_parent = NodeFactory(creator=user)
+ node_parent_child = NodeRelationFactory(child=node_child, parent=node_parent)
+ url = '/{}nodes/{}/relationships/linked_nodes/'.format(
+ API_BASE, node_parent_child.child._id
+ )
+ res = app.put_json_api(
+ url, {
+ 'data': [{
+ 'type': 'linked_nodes',
+ 'id': node_parent_child.parent._id
+ }]
+ },
+ auth=user.auth, expect_errors=True
+ )
+
+ assert res.status_code == 400
+
+ # test_linking_node_to_itself _on_update
+ node_self = NodeFactory(creator=user)
+ url = '/{}nodes/{}/relationships/linked_nodes/'.format(
+ API_BASE, node_self._id
+ )
+ res = app.put_json_api(
+ url_private, make_payload([node_linking_private._id]),
+ auth=user.auth, expect_errors=True
+ )
+
+ assert res.status_code == 400
+
def test_node_links_and_relationship_represent_same_nodes(
self, app, user, node_admin, node_contrib, node_linking_private, url_private):
node_linking_private.add_pointer(node_admin, auth=Auth(user))
diff --git a/api_tests/nodes/views/test_node_linked_registrations.py b/api_tests/nodes/views/test_node_linked_registrations.py
index 7c10f39e93c..43cea6dbcd0 100644
--- a/api_tests/nodes/views/test_node_linked_registrations.py
+++ b/api_tests/nodes/views/test_node_linked_registrations.py
@@ -7,6 +7,7 @@
NodeFactory,
OSFGroupFactory,
RegistrationFactory,
+ NodeRelationFactory,
)
from osf.utils.permissions import READ
from rest_framework import exceptions
@@ -276,8 +277,8 @@ def test_rw_contributor_can_create_linked_registrations_relationship(
assert registration._id in linked_registrations
def test_cannot_create_linked_registrations_relationship(
- self, make_request, user_admin_contrib, user_read_contrib,
- user_non_contrib, node_private):
+ self, app, make_request, user_admin_contrib, user_read_contrib,
+ user_non_contrib, node_private, make_payload):
# test_read_contributor_cannot_create_linked_registrations_relationship
registration = RegistrationFactory(is_public=True)
@@ -345,6 +346,26 @@ def test_cannot_create_linked_registrations_relationship(
assert res.status_code == 403
assert res.json['errors'][0]['detail'] == exceptions.PermissionDenied.default_detail
+ # test_cannot_create_relationship_with_child_registration
+ child_reg = RegistrationFactory(creator=user_admin_contrib)
+ NodeRelationFactory(child=child_reg, parent=node_private)
+ url = '/{}nodes/{}/relationships/linked_registrations/'.format(
+ API_BASE, node_private._id)
+ data = make_payload(registration_id=child_reg._id)
+ res = app.post_json_api(url, data, auth=user_admin_contrib.auth, expect_errors=True)
+ assert res.status_code == 400
+ assert res.json['errors'][0]['detail'] == 'Target Node \'{}\' is already a child of \'{}\'.'.format(child_reg._id, node_private._id)
+
+ # test_cannot_create_link_registration_to_itself
+ res = make_request(
+ node_id=node_private._id,
+ reg_id=node_private._id,
+ auth=user_admin_contrib.auth,
+ expect_errors=True
+ )
+ assert res.status_code == 400
+ assert res.json['errors'][0]['detail'] == 'Cannot link node \'{}\' to itself.'.format(node_private._id)
+
def test_create_linked_registrations_relationship_registration_already_in_linked_registrations_returns_no_content(
self, make_request, registration, node_private, user_admin_contrib):
res = make_request(
@@ -484,7 +505,7 @@ def test_empty_payload_removes_existing_linked_registrations(
assert registration._id not in linked_registrations
def test_cannot_update_linked_registrations_relationship(
- self, make_request, user_read_contrib, user_non_contrib, node_private):
+ self, app, make_request, make_payload, user_read_contrib, user_non_contrib, node_private, user_admin_contrib):
# test_read_contributor_cannot_update_linked_registrations_relationship
registration = RegistrationFactory(is_public=True)
@@ -518,6 +539,25 @@ def test_cannot_update_linked_registrations_relationship(
assert res.status_code == 401
assert res.json['errors'][0]['detail'] == exceptions.NotAuthenticated.default_detail
+ # test_cannot_update_relationship_with_child_registration
+ child_reg = RegistrationFactory(creator=user_admin_contrib)
+ NodeRelationFactory(child=child_reg, parent=node_private)
+ url = '/{}nodes/{}/relationships/linked_registrations/'.format(
+ API_BASE, node_private._id)
+ data = make_payload(registration_id=child_reg._id)
+ res = app.put_json_api(url, data, auth=user_admin_contrib.auth, expect_errors=True)
+ assert res.status_code == 400
+ assert res.json['errors'][0]['detail'] == 'Target Node \'{}\' is already a child of \'{}\'.'.format(child_reg._id, node_private._id)
+
+ # test_cannot_update_link_registration_to_itself
+ res = make_request(
+ node_id=node_private._id,
+ reg_id=node_private._id,
+ auth=user_admin_contrib.auth,
+ expect_errors=True
+ )
+ assert res.status_code == 400
+ assert res.json['errors'][0]['detail'] == 'Cannot link node \'{}\' to itself.'.format(node_private._id)
@pytest.mark.django_db
class TestNodeLinkedRegistrationsRelationshipDelete(
diff --git a/api_tests/nodes/views/test_node_links_list.py b/api_tests/nodes/views/test_node_links_list.py
index 90ab6ba4560..f5d6ebad68d 100644
--- a/api_tests/nodes/views/test_node_links_list.py
+++ b/api_tests/nodes/views/test_node_links_list.py
@@ -509,17 +509,12 @@ def test_create_fake_node_pointing_to_contributing_node(
def test_create_node_pointer_to_itself(
self, app, user, public_project,
public_url, make_payload):
- with assert_latest_log(NodeLog.POINTER_CREATED, public_project):
- point_to_itself_payload = make_payload(id=public_project._id)
- res = app.post_json_api(
- public_url,
- point_to_itself_payload,
- auth=user.auth)
- res_json = res.json['data']
- assert res.status_code == 201
- assert res.content_type == 'application/vnd.api+json'
- embedded = res_json['embeds']['target_node']['data']['id']
- assert embedded == public_project._id
+ point_to_itself_payload = make_payload(id=public_project._id)
+ res = app.post_json_api(
+ public_url,
+ point_to_itself_payload,
+ auth=user.auth, expect_errors=True)
+ assert res.status_code == 400
def test_create_node_pointer_errors(
self, app, user, user_two, public_project,
@@ -984,29 +979,24 @@ def test_bulk_creates_node_pointers_contributing_node_to_non_contributing_node(
def test_bulk_creates_node_pointer_to_itself(
self, app, user, public_project, public_url):
- with assert_latest_log(NodeLog.POINTER_CREATED, public_project):
- point_to_itself_payload = {
- 'data': [{
- 'type': 'node_links',
- 'relationships': {
- 'nodes': {
- 'data': {
- 'type': 'nodes',
- 'id': public_project._id
- }
+ point_to_itself_payload = {
+ 'data': [{
+ 'type': 'node_links',
+ 'relationships': {
+ 'nodes': {
+ 'data': {
+ 'type': 'nodes',
+ 'id': public_project._id
}
}
- }]
- }
+ }
+ }]
+ }
- res = app.post_json_api(
- public_url, point_to_itself_payload,
- auth=user.auth, bulk=True)
- assert res.status_code == 201
- assert res.content_type == 'application/vnd.api+json'
- res_json = res.json['data']
- embedded = res_json[0]['embeds']['target_node']['data']['id']
- assert embedded == public_project._id
+ res = app.post_json_api(
+ public_url, point_to_itself_payload,
+ auth=user.auth, bulk=True, expect_errors=True)
+ assert res.status_code == 400
def test_bulk_creates_node_pointer_already_connected(
self, app, user, public_project,
diff --git a/api_tests/nodes/views/test_node_list.py b/api_tests/nodes/views/test_node_list.py
index cda298f13e6..cbd3de4b8c4 100644
--- a/api_tests/nodes/views/test_node_list.py
+++ b/api_tests/nodes/views/test_node_list.py
@@ -7,6 +7,7 @@
from api_tests.subjects.mixins import SubjectsFilterMixin
from framework.auth.core import Auth
from osf.models import AbstractNode, Node, NodeLog
+from osf.models.licenses import NodeLicense
from osf.utils.sanitize import strip_html
from osf.utils import permissions
from osf_tests.factories import (
@@ -1957,6 +1958,140 @@ def test_create_project_errors(
assert res.status_code == 400
assert res.json['errors'][0]['detail'] == 'Title cannot exceed 512 characters.'
+@pytest.mark.django_db
+class TestNodeLicenseOnCreate:
+
+ @pytest.fixture()
+ def user(self):
+ return AuthUserFactory()
+
+ @pytest.fixture()
+ def url(self):
+ return '/{}nodes/'.format(API_BASE)
+
+ @pytest.fixture()
+ def license_name(self):
+ return 'MIT License'
+
+ @pytest.fixture()
+ def node_license(self, license_name):
+ return NodeLicense.objects.filter(name=license_name).first()
+
+ @pytest.fixture()
+ def make_payload(self):
+ def payload(
+ license_id=None, license_year=None, copyright_holders=None):
+ attributes = {}
+
+ if license_year and copyright_holders:
+ attributes = {
+ 'title': 'new title',
+ 'category': 'project',
+ 'tags': ['foo', 'bar'],
+ 'node_license': {
+ 'copyright_holders': copyright_holders,
+ 'year': license_year,
+ }
+ }
+ elif license_year:
+ attributes = {
+ 'title': 'new title',
+ 'category': 'project',
+ 'tags': ['foo', 'bar'],
+ 'node_license': {
+ 'year': license_year,
+ }
+ }
+ elif copyright_holders:
+ attributes = {
+ 'title': 'new title',
+ 'category': 'project',
+ 'tags': ['foo', 'bar'],
+ 'node_license': {
+ 'copyright_holders': copyright_holders
+ }
+ }
+
+ return {
+ 'data': {
+ 'type': 'nodes',
+ 'attributes': attributes,
+ 'relationships': {
+ 'license': {
+ 'data': {
+ 'type': 'licenses',
+ 'id': license_id
+ }
+ }
+ }
+ }
+ } if license_id else {
+ 'data': {
+ 'type': 'nodes',
+ 'attributes': attributes
+ }
+ }
+ return payload
+
+ def test_node_license_on_create(
+ self, app, user, url, node_license, make_payload):
+ data = make_payload(
+ copyright_holders=['Haagen', 'Dazs'],
+ license_year='2200',
+ license_id=node_license._id
+ )
+ res = app.post_json_api(
+ url, data, auth=user.auth)
+ assert res.json['data']['attributes']['node_license']['year'] == '2200'
+ assert res.json['data']['attributes']['node_license']['copyright_holders'] == ['Haagen', 'Dazs']
+ assert res.json['data']['relationships']['license']['data']['id'] == node_license._id
+
+ def test_create_node_license_errors(
+ self, app, url, user, node_license, make_payload):
+
+ # test_creating_a_node_license_without_a_license_id
+ data = make_payload(
+ license_year='2200',
+ copyright_holders=['Ben', 'Jerry']
+ )
+ res = app.post_json_api(
+ url, data, auth=user.auth, expect_errors=True)
+ assert res.status_code == 400
+ assert res.json['errors'][0]['detail'] == 'License ID must be provided for a Node License.'
+
+ # test_creating_a_node_license_without_a_copyright_holder
+ data = make_payload(
+ license_year='2200',
+ license_id=node_license._id
+ )
+ res = app.post_json_api(
+ url, data,
+ auth=user.auth, expect_errors=True)
+ assert res.status_code == 400
+ assert res.json['errors'][0]['detail'] == 'copyrightHolders must be specified for this license'
+
+ # test_creating_a_node_license_without_a_year
+ data = make_payload(
+ copyright_holders=['Baskin', 'Robbins'],
+ license_id=node_license._id
+ )
+ res = app.post_json_api(
+ url, data,
+ auth=user.auth, expect_errors=True)
+ assert res.status_code == 400
+ assert res.json['errors'][0]['detail'] == 'year must be specified for this license'
+
+ # test_creating_a_node_license_with_an_invalid_ID
+ data = make_payload(
+ license_year='2200',
+ license_id='invalid id',
+ copyright_holders=['Ben', 'Jerry']
+ )
+ res = app.post_json_api(
+ url, data,
+ auth=user.auth, expect_errors=True)
+ assert res.status_code == 404
+ assert res.json['errors'][0]['detail'] == 'Unable to find specified license.'
@pytest.mark.django_db
class TestNodeBulkCreate:
diff --git a/api_tests/wb/views/test_wb_hooks.py b/api_tests/wb/views/test_wb_hooks.py
index b1933b2859a..89bf3a18f10 100644
--- a/api_tests/wb/views/test_wb_hooks.py
+++ b/api_tests/wb/views/test_wb_hooks.py
@@ -5,6 +5,8 @@
from addons.osfstorage.models import OsfStorageFolder
from framework.auth import signing
+from api.caching.tasks import update_storage_usage_cache
+
from osf_tests.factories import (
AuthUserFactory,
ProjectFactory,
@@ -326,6 +328,44 @@ def test_node_in_params_does_not_exist(self, app, file, root_node, user, folder)
res = app.post_json(move_url, signed_payload, expect_errors=True)
assert res.status_code == 404
+ def test_storage_usage_move_within_node(self, app, node, signed_payload, move_url):
+ """
+ Checking moves within a node, since the net value hasn't changed the cache will remain expired at None.
+ """
+ assert node.storage_usage is None
+
+ res = app.post_json(move_url, signed_payload)
+
+ assert res.status_code == 200
+ assert node.storage_usage is None # this is intentional, the cache shouldn't be touched
+
+ def test_storage_usage_move_between_nodes(self, app, node, node_two, file, root_node, user, node_two_root_node, move_url):
+ """
+ Checking storage usage when moving files outside a node mean both need to be recalculated, as both values have
+ changed.
+ """
+
+ assert node.storage_usage is None # the cache starts expired, but there is 1337 bytes in there
+ assert node_two.storage_usage is None # zero bytes here
+
+ signed_payload = sign_payload(
+ {
+ 'source': file._id,
+ 'target': node._id,
+ 'user': user._id,
+ 'destination': {
+ 'parent': node_two_root_node._id,
+ 'target': node_two._id,
+ 'name': 'test_file',
+ }
+ }
+ )
+ res = app.post_json(move_url, signed_payload)
+ assert res.status_code == 200
+
+ assert node.storage_usage is None
+ assert node_two.storage_usage == 1337
+
@pytest.mark.django_db
class TestMovePreprint():
@@ -802,6 +842,50 @@ def test_invalid_payload(self, app, copy_url):
assert res.status_code == 400
assert res.json['errors'][0]['detail'] == 'Invalid Payload'
+ def test_storage_usage_copy_within_node(self, app, node, file, signed_payload, copy_url):
+ """
+ Checking copys within a node, since the net size will double the storage usage should be the file size * 2
+ """
+ assert node.storage_usage is None
+
+ res = app.post_json(copy_url, signed_payload)
+
+ assert res.status_code == 201
+ assert node.storage_usage == file.versions.last().metadata['size'] * 2
+
+ def test_storage_usage_copy_between_nodes(self, app, node, node_two, file, user, node_two_root_node, copy_url):
+ """
+ Checking storage usage when copying files to outside a node means only the destination should be recalculated.
+ """
+
+ assert node.storage_usage is None # The node cache starts expired, but there is 1337 bytes in there
+ assert node_two.storage_usage is None # There are zero bytes in node_two
+
+ signed_payload = sign_payload(
+ {
+ 'source': file._id,
+ 'target': node._id,
+ 'user': user._id,
+ 'destination': {
+ 'parent': node_two_root_node._id,
+ 'target': node_two._id,
+ 'name': 'test_file',
+ }
+ }
+ )
+ res = app.post_json(copy_url, signed_payload)
+ assert res.status_code == 201
+
+ # The node cache is None because it's value should be unchanged --
+ assert node.storage_usage is None
+
+ # But there's really 1337 bytes in the node
+ update_storage_usage_cache(node._id)
+ assert node.storage_usage == 1337
+
+ # And we have exactly 1337 bytes copied in node_two
+ assert node_two.storage_usage == 1337
+
@pytest.mark.django_db
@pytest.mark.enable_quickfiles_creation
diff --git a/framework/auth/views.py b/framework/auth/views.py
index a317cdd51a8..9e1bfeb4e24 100644
--- a/framework/auth/views.py
+++ b/framework/auth/views.py
@@ -67,9 +67,13 @@ def reset_password_get(auth, uid=None, token=None):
user_obj.verification_key_v2 = generate_verification_key(verification_type='password')
user_obj.save()
+ #override routes.py login_url to redirect to dashboard
+ service_url = web_url_for('dashboard', _absolute=True)
+
return {
'uid': user_obj._id,
'token': user_obj.verification_key_v2['token'],
+ 'login_url': service_url,
}
@@ -138,7 +142,11 @@ def forgot_password_get(auth):
if auth.logged_in:
return auth_logout(redirect_url=request.url)
- return {}
+ #overriding the routes.py sign in url to redirect to the dashboard after login
+ context = {}
+ context['login_url'] = web_url_for('dashboard', _absolute=True)
+
+ return context
def forgot_password_post():
diff --git a/osf/admin.py b/osf/admin.py
index 39c614ae059..601bcb3bb22 100644
--- a/osf/admin.py
+++ b/osf/admin.py
@@ -20,7 +20,7 @@ def formfield_for_manytomany(self, db_field, request, **kwargs):
Restricts preprint/node/osfgroup django groups from showing up in the user's groups list in the admin app
"""
if db_field.name == 'groups':
- kwargs['queryset'] = Group.objects.exclude(Q(name__startswith='preprint_') | Q(name__startswith='node_') | Q(name__startswith='osfgroup_'))
+ kwargs['queryset'] = Group.objects.exclude(Q(name__startswith='preprint_') | Q(name__startswith='node_') | Q(name__startswith='osfgroup_') | Q(name__startswith='collections_'))
return super(OSFUserAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
def save_related(self, request, form, formsets, change):
@@ -28,7 +28,7 @@ def save_related(self, request, form, formsets, change):
Since m2m fields overridden with new form data in admin app, preprint groups/node/osfgroup groups (which are now excluded from being selections)
are removed. Manually re-adds preprint/node groups after adding new groups in form.
"""
- groups_to_preserve = list(form.instance.groups.filter(Q(name__startswith='preprint_') | Q(name__startswith='node_') | Q(name__startswith='osfgroup_')))
+ groups_to_preserve = list(form.instance.groups.filter(Q(name__startswith='preprint_') | Q(name__startswith='node_') | Q(name__startswith='osfgroup_') | Q(name__startswith='collections_')))
super(OSFUserAdmin, self).save_related(request, form, formsets, change)
if 'groups' in form.cleaned_data:
for group in groups_to_preserve:
diff --git a/osf/models/mixins.py b/osf/models/mixins.py
index 9ab02ba64de..8fd6a9b30ac 100644
--- a/osf/models/mixins.py
+++ b/osf/models/mixins.py
@@ -337,13 +337,11 @@ def add_node_link(self, node, auth, save=True):
:param bool save: Save changes
:return: Created pointer
"""
- # Fail if node already in nodes / pointers. Note: cast node and node
- # to primary keys to test for conflicts with both nodes and pointers
- # contained in `self.nodes`.
- if NodeRelation.objects.filter(parent=self, child=node, is_node_link=True).exists():
- raise ValueError(
- 'Link to node {0} already exists'.format(node._id)
- )
+ try:
+ self.check_node_link(child_node=node, parent_node=self)
+ self.check_node_link(child_node=self, parent_node=node)
+ except ValueError as e:
+ raise ValueError(e.message)
if self.is_registration:
raise self.state_error('Cannot add a node link to a registration')
@@ -381,6 +379,21 @@ def add_node_link(self, node, auth, save=True):
add_pointer = add_node_link # For v1 compat
+ def check_node_link(self, child_node, parent_node):
+ if child_node._id == parent_node._id:
+ raise ValueError(
+ 'Cannot link node \'{}\' to itself.'.format(child_node._id)
+ )
+ existant_relation = NodeRelation.objects.filter(parent=parent_node, child=child_node).first()
+ if existant_relation and existant_relation.is_node_link:
+ raise ValueError(
+ 'Target Node \'{}\' already pointed to by \'{}\'.'.format(child_node._id, parent_node._id)
+ )
+ elif existant_relation and not existant_relation.is_node_link:
+ raise ValueError(
+ 'Target Node \'{}\' is already a child of \'{}\'.'.format(child_node._id, parent_node._id)
+ )
+
def rm_node_link(self, node_relation, auth):
"""Remove a pointer.
diff --git a/osf/models/user.py b/osf/models/user.py
index 8cb415846de..3061ddf7e24 100644
--- a/osf/models/user.py
+++ b/osf/models/user.py
@@ -10,6 +10,7 @@
from flask import Request as FlaskRequest
from framework import analytics
from guardian.shortcuts import get_perms
+from past.builtins import basestring
# OSF imports
import itsdangerous
@@ -453,13 +454,31 @@ def unconfirmed_emails(self):
@property
def social_links(self):
+ """
+ Returns a dictionary of formatted social links for a user.
+
+ Social account values which are stored as account names are
+ formatted into appropriate social links. The 'type' of each
+ respective social field value is dictated by self.SOCIAL_FIELDS.
+
+ I.e. If a string is expected for a specific social field that
+ permits multiple accounts, a single account url will be provided for
+ the social field to ensure adherence with self.SOCIAL_FIELDS.
+ """
social_user_fields = {}
for key, val in self.social.items():
if val and key in self.SOCIAL_FIELDS:
- if not isinstance(val, basestring):
- social_user_fields[key] = val
+ if isinstance(self.SOCIAL_FIELDS[key], basestring):
+ if isinstance(val, basestring):
+ social_user_fields[key] = self.SOCIAL_FIELDS[key].format(val)
+ else:
+ # Only provide the first url for services where multiple accounts are allowed
+ social_user_fields[key] = self.SOCIAL_FIELDS[key].format(val[0])
else:
- social_user_fields[key] = self.SOCIAL_FIELDS[key].format(val)
+ if isinstance(val, basestring):
+ social_user_fields[key] = [val]
+ else:
+ social_user_fields[key] = val
return social_user_fields
@property
diff --git a/osf_tests/test_node.py b/osf_tests/test_node.py
index 5e24ef050f2..8b7bea86afa 100644
--- a/osf_tests/test_node.py
+++ b/osf_tests/test_node.py
@@ -286,8 +286,6 @@ def test_get_children_with_links(self):
NodeFactory(parent=grandchild3)
greatgrandchild_1 = NodeFactory(parent=grandchild_1)
- child.add_node_link(root, auth=Auth(root.creator))
- child.add_node_link(greatgrandchild_1, auth=Auth(greatgrandchild_1.creator))
greatgrandchild_1.add_node_link(child, auth=Auth(child.creator))
assert 20 == len(Node.objects.get_children(root))
@@ -2733,7 +2731,6 @@ def test_get_descendants_recursive_cyclic(self, user, root, auth):
point1 = ProjectFactory(creator=user, parent=root)
point2 = ProjectFactory(creator=user, parent=root)
point1.add_pointer(point2, auth=auth)
- point2.add_pointer(point1, auth=auth)
descendants = list(point1.get_descendants_recursive())
assert len(descendants) == 1
diff --git a/osf_tests/test_user.py b/osf_tests/test_user.py
index c460e29d410..7f44832f85e 100644
--- a/osf_tests/test_user.py
+++ b/osf_tests/test_user.py
@@ -2110,21 +2110,23 @@ def test_profile_website_unchanged(self):
def test_various_social_handles(self):
self.user.social = {
'profileWebsites': ['http://cos.io/'],
- 'twitter': 'OSFramework',
- 'github': 'CenterForOpenScience'
+ 'twitter': ['OSFramework'],
+ 'github': ['CenterForOpenScience'],
+ 'scholar': 'ztt_j28AAAAJ'
}
self.user.save()
assert self.user.social_links == {
'profileWebsites': ['http://cos.io/'],
'twitter': 'http://twitter.com/OSFramework',
- 'github': 'http://github.com/CenterForOpenScience'
+ 'github': 'http://github.com/CenterForOpenScience',
+ 'scholar': 'http://scholar.google.com/citations?user=ztt_j28AAAAJ'
}
def test_multiple_profile_websites(self):
self.user.social = {
'profileWebsites': ['http://cos.io/', 'http://thebuckstopshere.com', 'http://dinosaurs.com'],
- 'twitter': 'OSFramework',
- 'github': 'CenterForOpenScience'
+ 'twitter': ['OSFramework'],
+ 'github': ['CenterForOpenScience']
}
self.user.save()
assert self.user.social_links == {
diff --git a/package.json b/package.json
index 70eab34d83b..c5ea698507d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "OSF",
- "version": "19.23.0",
+ "version": "19.24.0",
"description": "Facilitating Open Science",
"repository": "https://github.com/CenterForOpenScience/osf.io",
"author": "Center for Open Science",
diff --git a/scripts/retract_registrations.py b/scripts/retract_registrations.py
index 534f2287258..592788e7931 100644
--- a/scripts/retract_registrations.py
+++ b/scripts/retract_registrations.py
@@ -49,7 +49,7 @@ def main(dry_run=True):
'registration': parent_registration._id,
'retraction_id': parent_registration.retraction._id,
},
- auth=Auth(parent_registration.retraction.initiated_by),
+ auth=None,
)
retraction.save()
parent_registration.update_search()
diff --git a/tests/test_notifications.py b/tests/test_notifications.py
index 5d9b50a0c61..a237f23d728 100644
--- a/tests/test_notifications.py
+++ b/tests/test_notifications.py
@@ -945,7 +945,6 @@ def test_format_data_user_subscriptions_includes_private_parent_if_configured_ch
def test_format_data_user_subscriptions_if_children_points_to_parent(self):
private_project = factories.ProjectFactory(creator=self.user)
node = factories.NodeFactory(parent=private_project, creator=self.user)
- node.add_pointer(private_project, Auth(self.user))
node.save()
node_comments_subscription = factories.NotificationSubscriptionFactory(
_id=node._id + '_' + 'comments',
diff --git a/tests/test_views.py b/tests/test_views.py
index 591b81dec9d..db884bcd5c4 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -1049,7 +1049,6 @@ def test_get_node_with_children(self):
def test_get_node_with_child_linked_to_parent(self):
project = ProjectFactory(creator=self.user)
child1 = NodeFactory(parent=project, creator=self.user)
- child1.add_pointer(project, Auth(self.user))
child1.save()
url = project.api_url_for('get_node_tree')
res = self.app.get(url, auth=self.user.auth)
@@ -3144,7 +3143,6 @@ def test_get_pointed_private(self):
def test_can_template_project_linked_to_each_other(self):
project2 = ProjectFactory(creator=self.user)
self.project.add_pointer(project2, auth=Auth(user=self.user))
- project2.add_pointer(self.project, auth=Auth(user=self.user))
template = self.project.use_as_template(auth=Auth(user=self.user))
assert_true(template)
diff --git a/website/static/js/anonymousLogActionsList.json b/website/static/js/anonymousLogActionsList.json
index 28084915411..e5ec33b6b3f 100644
--- a/website/static/js/anonymousLogActionsList.json
+++ b/website/static/js/anonymousLogActionsList.json
@@ -61,13 +61,14 @@
"embargo_completed_no_user" : "Embargo for a project completed",
"embargo_terminated": "Embargo for a project ended",
"retraction_initiated" : "A user initiated a withdrawal of a registration of a project",
- "retraction_initiated_no_user" : "A withdrawal of registration of a project is proposed",
- "retraction_approved" : "A user approved a withdrawal of registration of a project",
- "retraction_cancelled" : "A user cancelled a withdrawal of registration of a project",
+ "retraction_initiated_no_user" : "A withdrawal of a registration of a project was proposed",
+ "retraction_approved" : "A user approved a withdrawal of a registration of a project",
+ "retraction_approved_no_user": "A withdrawal of a registration of ${node} was approved",
+ "retraction_cancelled" : "A user cancelled a withdrawal of a registration of a project",
"registration_initiated" : "A user initiated a registration of a project",
- "registration_approved" : "A user approved registration of a project",
- "registration_approved_no_user" : "Registration of a project approved",
- "registration_cancelled" : "A user cancelled registration of a project",
+ "registration_approved" : "A user approved a registration of a project",
+ "registration_approved_no_user" : "Registration of a project was approved",
+ "registration_cancelled" : "A user cancelled a registration of a project",
"node_created" : "A user created a project",
"node_forked" : "A user created fork from a project",
"node_removed" : "A user removed a project",
diff --git a/website/static/js/logActionsList.json b/website/static/js/logActionsList.json
index f35835ffebc..ae7673ec145 100644
--- a/website/static/js/logActionsList.json
+++ b/website/static/js/logActionsList.json
@@ -60,14 +60,15 @@
"embargo_completed": "${user} completed embargo of ${node}",
"embargo_completed_no_user": "Embargo for ${node} completed",
"embargo_terminated": "Embargo for ${node} ended",
- "retraction_initiated": "${user} initiated withdrawal of registration of ${node}",
- "retraction_initiated_no_user": "A withdrawal of registration of ${node} is proposed",
- "retraction_approved": "${user} approved withdrawal of registration of ${node}",
- "retraction_cancelled": "${user} cancelled withdrawal of registration of ${node}",
- "registration_initiated": "${user} initiated registration of ${node}",
- "registration_approved": "${user} approved registration of ${node}",
- "registration_approved_no_user": "Registration of ${node} approved",
- "registration_cancelled": "${user} cancelled registration of ${node}",
+ "retraction_initiated": "${user} initiated withdrawal of a registration of ${node}",
+ "retraction_initiated_no_user": "A withdrawal of a registration of ${node} was proposed",
+ "retraction_approved": "${user} approved a withdrawal of a registration of ${node}",
+ "retraction_approved_no_user": "A withdrawal of a registration of ${node} was approved",
+ "retraction_cancelled": "${user} cancelled withdrawal of a registration of ${node}",
+ "registration_initiated": "${user} initiated a registration of ${node}",
+ "registration_approved": "${user} approved a registration of ${node}",
+ "registration_approved_no_user": "Registration of ${node} was approved",
+ "registration_cancelled": "${user} cancelled a registration of ${node}",
"node_created": "${user} created ${node}",
"node_forked": "${user} created fork from ${forked_from}",
"node_removed": "${user} removed ${node}",
diff --git a/website/static/js/logTextParser.js b/website/static/js/logTextParser.js
index 489c17e0a5d..2c296593ffa 100644
--- a/website/static/js/logTextParser.js
+++ b/website/static/js/logTextParser.js
@@ -662,7 +662,7 @@ var LogPieces = {
googledrive_folder: {
view: function(ctrl, logObject){
- var folder = logObject.attributes.params.folder;
+ var folder = logObject.attributes.params.folder_name;
if(paramIsReturned(folder, logObject)){
return m('span', folder === '/' ? '(Full Google Drive)' : folder);
}
diff --git a/website/templates/emails/pending_embargo_admin.html.mako b/website/templates/emails/pending_embargo_admin.html.mako
index b452d3fec51..9be83fd2cfd 100644
--- a/website/templates/emails/pending_embargo_admin.html.mako
+++ b/website/templates/emails/pending_embargo_admin.html.mako
@@ -11,16 +11,16 @@
${initiated_by} initiated an embargoed registration of ${project_name}. The proposed registration can be viewed here: ${registration_link}.
% endif
- If approved, a registration will be created for the project and it will remain private until it is withdrawn, manually
- made public, or the embargo end date has passed on ${embargo_end_date.date()}.
+ If approved, a registration will be created for the project, and it will remain private until it is withdrawn,
+ it is manually made public, or the embargo end date is passed on ${embargo_end_date.date()}.
To approve this embargo, click the following link: ${approval_link}.
To cancel this embargo, click the following link: ${disapproval_link}.
- Note: Clicking the disapproval link will immediately cancel the pending embargo and the
- registration will remain in draft state. If you neither approve nor disapprove the embargo
- within ${approval_time_span} hours from midnight tonight (EDT) the registration will remain
+ Note: Clicking the disapproval link will immediately cancel the pending embargo and the
+ registration will remain in draft state. If you neither approve nor disapprove the embargo
+ within ${approval_time_span} hours from midnight tonight (EDT) the registration will remain
private and enter into an embargoed state.
Sincerely yours,
diff --git a/website/templates/emails/pending_embargo_non_admin.html.mako b/website/templates/emails/pending_embargo_non_admin.html.mako
index 3d5b32f85f3..bbd302ba4f1 100644
--- a/website/templates/emails/pending_embargo_non_admin.html.mako
+++ b/website/templates/emails/pending_embargo_non_admin.html.mako
@@ -7,8 +7,8 @@
We just wanted to let you know that ${initiated_by} has initiated an embargoed registration for the following pending registration: ${registration_link}.
- If approved, a registration will be created for the project, viewable here: ${registration_link}, and it will remain
- private until it is withdrawn, manually made public, or the embargo end date has passed on ${embargo_end_date.date()}.
+ If approved, a registration will be created for the project, viewable here: ${registration_link}, and it will remain
+ private until it is withdrawn, it is manually made public, or the embargo end date is passed on ${embargo_end_date.date()}.
Sincerely yours,
diff --git a/website/templates/public/resetpassword.mako b/website/templates/public/resetpassword.mako
index f417ea3495a..39d5edc96bf 100644
--- a/website/templates/public/resetpassword.mako
+++ b/website/templates/public/resetpassword.mako
@@ -96,6 +96,12 @@
%def>
+<%def name="javascript()">
+
+%def>
+
<%def name="javascript_bottom()">