Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
2911fb3
handle django core validation error in drf view
antkryt Jun 16, 2025
b11c15c
add unit tests
antkryt Jun 18, 2025
d7f8c7a
add registration_word for registration provider
antkryt Jun 30, 2025
42b3829
[ENG-8290] Allow collection search POST with token scope (#11201)
antkryt Jun 30, 2025
ef9c076
[ENG-8247] Ability to delete draft preprints from database (#11191)
ihorsokhanexoft Jun 30, 2025
9aa22ee
[ENG-8193] Fix issues with Preprint submission via API (#11185)
antkryt Jun 30, 2025
3e609ac
[ENG-8192] Ability to force archive registrations when OSFS Folders h…
antkryt Jun 30, 2025
ee1aeb4
add additional information to user admin (#11184)
antkryt Jun 30, 2025
33e86b3
upgrade django to 4.2.17 (#11173)
ihorsokhanexoft Jun 30, 2025
6c23802
added retry to avoid race condition (#11179)
ihorsokhanexoft Jun 30, 2025
ffaef7a
[ENG-8096] Admins on projects are unable to reject user access reques…
antkryt Jun 30, 2025
2beb44e
fix content overflow for node page (#11182)
antkryt Jun 30, 2025
689aa78
don't add multiple group perms for preprint provider (#11159)
antkryt Jun 30, 2025
a02120f
fixed children/parent fields in admin templates (#11156)
ihorsokhanexoft Jun 30, 2025
2e03570
[ENG-7962] Fix User Setting Response Payload async mailchimp perferen…
Johnetordoff Jun 30, 2025
a035dbf
improved displaying of stashed urls and approval state in admin (#11193)
ihorsokhanexoft Jun 30, 2025
5af4c80
[ENG-5862] SPAM - Fix Wiki Spamming (#11171)
antkryt Jun 30, 2025
fdfbcc4
switch to new UI when user views draft registration file (#11144)
ihorsokhanexoft Jun 30, 2025
6062cef
[ENG-7929] Ability to move registrations to draft state (#11153)
ihorsokhanexoft Jul 1, 2025
4391db2
added a route to download node metadata (#11215)
ihorsokhanexoft Jul 1, 2025
a50008c
add exception handling to /review_actions/ endpoint
antkryt Jul 8, 2025
686b581
Merge pull request #11211 from antkryt/feature/ENG-6135
adlius Jul 9, 2025
2f45c6a
Merge pull request #11223 from antkryt/fix/ENG-8193
adlius Jul 9, 2025
6dbcdda
[ENG-8246] Fixed deletion of maintenance alerts in admin (#11226)
ihorsokhanexoft Jul 16, 2025
76f601c
[ENG-8325] Public column does not display the visibility status of ch…
ihorsokhanexoft Jul 16, 2025
95be0e0
API: Allow /v2/users/me/preprints list view to filter by title
ihorsokhanexoft Jul 16, 2025
cc51cb3
[ENG-8224] Fixed force archive template with registration addons (#11…
ihorsokhanexoft Jul 16, 2025
99e26cb
add brand relationship to collectionprovider
Johnetordoff Jul 16, 2025
91672c0
Add collections scopes to FULL_READ and FULL_WRITE
futa-ikeda Jul 16, 2025
f235725
Merge pull request #11231 from futa-ikeda/collections-scopes
Johnetordoff Jul 17, 2025
82cdfff
Merge pull request #11230 from Johnetordoff/add-brand-to-collection-p…
Johnetordoff Jul 17, 2025
7f3a668
[ENG-8936] API: Allow /v2/users/me/preprints list view to filter by t…
ihorsokhanexoft Jul 21, 2025
707f05c
fix categories for sendgrid emails (#11236)
antkryt Jul 21, 2025
7f6d5e9
[ENG-8401] Fixed preprint downloading (#11238)
ihorsokhanexoft Jul 22, 2025
189854c
[ENG-8216] Fixed children deletion on a node page in admin (#11237)
ihorsokhanexoft Jul 22, 2025
69b2d90
[ENG-7979] Registrations pending moderation that have components also…
antkryt Jul 24, 2025
a8084f1
added academiaInstitution in social-schema, fixed True value of 'ongo…
ihorsokhanexoft Jul 24, 2025
61a072f
Merge remote-tracking branch 'upstream/develop' into feature/pbs-25-13
adlius Jul 29, 2025
8d9a26f
[ENG-8401] Earlier preprint versions download the current file (#11245)
ihorsokhanexoft Jul 31, 2025
8745da4
[ENG-8462] Institution setup fixes (#11241)
antkryt Jul 31, 2025
22c28fb
fixed affiliation update for write contributors in registrations
ihorsokhanexoft Aug 7, 2025
08ae6aa
removed redundant blank line
ihorsokhanexoft Aug 7, 2025
3fce62a
fixed tests
ihorsokhanexoft Aug 8, 2025
6ce160d
fixed tests
ihorsokhanexoft Aug 8, 2025
e45f2a6
fixed a test
ihorsokhanexoft Aug 8, 2025
3487223
[ENG-8514] Remove CSRF protection from reset password api v2 POST (#1…
antkryt Aug 15, 2025
7a03e98
add background color prop to Brand (#11254)
antkryt Aug 15, 2025
cf8ecbc
rebased onto 25-16 and fixed conflicts
ihorsokhanexoft Aug 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions admin/brands/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Meta:
widgets = {
'primary_color': TextInput(attrs={'class': 'colorpicker'}),
'secondary_color': TextInput(attrs={'class': 'colorpicker'}),
'background_color': TextInput(attrs={'class': 'colorpicker'}),
'topnav_logo_image': TextInput(attrs={'placeholder': 'Logo should be max height of 40px', 'size': 200}),
'hero_logo_image': TextInput(
attrs={'placeholder': 'Logo image should be max height of 100px', 'size': 200}
Expand Down
8 changes: 8 additions & 0 deletions admin/brands/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,17 @@ def post(self, request, *args, **kwargs):
view = BrandChangeForm.as_view()
primary_color = request.POST.get('primary_color')
secondary_color = request.POST.get('secondary_color')
background_color = request.POST.get('background_color')

if not is_a11y(primary_color):
messages.warning(request, """The selected primary color is not a11y compliant.
For more information, visit https://color.a11y.com/""")
if not is_a11y(secondary_color):
messages.warning(request, """The selected secondary color is not a11y compliant.
For more information, visit https://color.a11y.com/""")
if background_color and not is_a11y(background_color):
messages.warning(request, """The selected background color is not a11y compliant.
For more information, visit https://color.a11y.com/""")
return view(request, *args, **kwargs)


Expand All @@ -109,11 +113,15 @@ def get_context_data(self, *args, **kwargs):
def post(self, request, *args, **kwargs):
primary_color = request.POST.get('primary_color')
secondary_color = request.POST.get('secondary_color')
background_color = request.POST.get('background_color')

if not is_a11y(primary_color):
messages.warning(request, """The selected primary color is not a11y compliant.
For more information, visit https://color.a11y.com/""")
if not is_a11y(secondary_color):
messages.warning(request, """The selected secondary color is not a11y compliant.
For more information, visit https://color.a11y.com/""")
if background_color and not is_a11y(background_color):
messages.warning(request, """The selected background color is not a11y compliant.
For more information, visit https://color.a11y.com/""")
return super().post(request, *args, **kwargs)
1 change: 1 addition & 0 deletions api/brands/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class BrandSerializer(JSONAPISerializer):

primary_color = ser.CharField(read_only=True, max_length=7)
secondary_color = ser.CharField(read_only=True, max_length=7)
background_color = ser.CharField(read_only=True, allow_null=True, max_length=7)

links = LinksField({
'self': 'get_absolute_url',
Expand Down
16 changes: 16 additions & 0 deletions api/nodes/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,22 @@ def has_object_permission(self, request, view, obj):
return obj.has_permission(auth.user, osf_permissions.WRITE)


class AdminOrWriteContributor(permissions.BasePermission):
acceptable_models = (AbstractNode, OSFUser, Institution, BaseAddonSettings, DraftRegistration)

def has_object_permission(self, request, view, obj):
if isinstance(obj, dict) and 'self' in obj:
obj = obj['self']

assert_resource_type(obj, self.acceptable_models)
auth = get_user_auth(request)

if request.method in permissions.SAFE_METHODS:
return obj.is_public or obj.can_view(auth)

return obj.has_permission(auth.user, osf_permissions.ADMIN) or obj.has_permission(auth.user, osf_permissions.WRITE)


class AdminOrPublic(permissions.BasePermission):

acceptable_models = (AbstractNode, OSFUser, Institution, BaseAddonSettings, DraftRegistration)
Expand Down
3 changes: 2 additions & 1 deletion api/registrations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
AdminOrPublic,
ExcludeWithdrawals,
NodeLinksShowIfVersion,
AdminOrWriteContributor,
)
from api.registrations.permissions import ContributorOrModerator, ContributorOrModeratorOrPublic
from api.registrations.serializers import (
Expand Down Expand Up @@ -682,7 +683,7 @@ class RegistrationInstitutionsRelationship(NodeInstitutionsRelationship, Registr
permission_classes = (
drf_permissions.IsAuthenticatedOrReadOnly,
base_permissions.TokenHasScope,
AdminOrPublic,
AdminOrWriteContributor,
)


Expand Down
1 change: 0 additions & 1 deletion api/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,6 @@ def get(self, request, *args, **kwargs):
)
return Response(status=status.HTTP_200_OK, data={'message': status_message, 'kind': kind, 'institutional': institutional})

@method_decorator(csrf_protect)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ def resource_factory(self):
return RegistrationFactory

# test override, write contribs can't update institution
def test_put_not_admin_but_affiliated(self, app, institution_one, node, node_institutions_url):
def test_put_not_admin_but_affiliated_read_permission(self, app, institution_one, node, node_institutions_url):
user = AuthUserFactory()
user.add_or_update_affiliated_institution(institution_one)
user.save()
node.add_contributor(user)
node.add_contributor(user, permissions=permissions.READ)
node.save()

res = app.put_json_api(
Expand All @@ -58,7 +58,25 @@ def test_put_not_admin_but_affiliated(self, app, institution_one, node, node_ins
assert res.status_code == 403
assert institution_one not in node.affiliated_institutions.all()

# test override, write contribs cannot delete
def test_put_not_admin_but_affiliated_and_write_permission(self, app, institution_one, node, node_institutions_url):
user = AuthUserFactory()
user.add_or_update_affiliated_institution(institution_one)
user.save()
node.add_contributor(user)
node.save()

res = app.put_json_api(
node_institutions_url,
self.create_payload([institution_one]),
expect_errors=True,
auth=user.auth
)

node.reload()
assert res.status_code == 200
assert institution_one in node.affiliated_institutions.all()

# test override, write contribs can delete
def test_delete_user_is_read_write(self, app, institution_one, node, node_institutions_url):
user = AuthUserFactory()
user.add_or_update_affiliated_institution(institution_one)
Expand All @@ -74,7 +92,7 @@ def test_delete_user_is_read_write(self, app, institution_one, node, node_instit
expect_errors=True
)

assert res.status_code == 403
assert res.status_code == 204

def test_read_write_contributor_can_add_affiliated_institution(
self, app, write_contrib, write_contrib_institution, node, node_institutions_url):
Expand All @@ -91,8 +109,8 @@ def test_read_write_contributor_can_add_affiliated_institution(
auth=write_contrib.auth,
expect_errors=True
)
assert res.status_code == 403
assert write_contrib_institution not in node.affiliated_institutions.all()
assert res.status_code == 201
assert write_contrib_institution in node.affiliated_institutions.all()

def test_read_write_contributor_can_remove_affiliated_institution(
self, app, write_contrib, write_contrib_institution, node, node_institutions_url):
Expand All @@ -111,8 +129,8 @@ def test_read_write_contributor_can_remove_affiliated_institution(
auth=write_contrib.auth,
expect_errors=True
)
assert res.status_code == 403
assert write_contrib_institution in node.affiliated_institutions.all()
assert res.status_code == 204
assert write_contrib_institution not in node.affiliated_institutions.all()

def test_user_with_institution_and_permissions_through_patch(self, app, user, institution_one, institution_two,
node, node_institutions_url):
Expand Down
5 changes: 2 additions & 3 deletions api_tests/users/views/test_user_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,7 @@ def test_get_invalid_email(self, app, url):
assert res.status_code == 200
assert not mock_send_mail.called

def test_post(self, app, url, user_one, csrf_token):
app.set_cookie(CSRF_COOKIE_NAME, csrf_token)
def test_post(self, app, url, user_one):
encoded_email = urllib.parse.quote(user_one.email)
url = f'{url}?email={encoded_email}'
res = app.get(url)
Expand All @@ -227,7 +226,7 @@ def test_post(self, app, url, user_one, csrf_token):
}
}

res = app.post_json_api(url, payload, headers={'X-CSRFToken': csrf_token})
res = app.post_json_api(url, payload)
user_one.reload()
assert res.status_code == 200
assert user_one.check_password('password2')
Expand Down
41 changes: 21 additions & 20 deletions osf/management/commands/force_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from addons.osfstorage.models import OsfStorageFile, OsfStorageFolder, OsfStorageFileNode
from framework import sentry
from framework.exceptions import HTTPError
from osf.models import AbstractNode, Node, NodeLog, Registration, BaseFileNode
from osf.models import Node, NodeLog, Registration, BaseFileNode
from osf.models.files import TrashedFileNode
from osf.exceptions import RegistrationStuckRecoverableException, RegistrationStuckBrokenException
from api.base.utils import waterbutler_api_url_for
Expand Down Expand Up @@ -285,32 +285,33 @@ def get_file_obj_from_log(log, reg):
try:
return BaseFileNode.objects.get(_id=log.params['urls']['view'].split('/')[4])
except KeyError:
path = log.params.get('path', '').split('/')
if log.action in ['addon_file_moved', 'addon_file_renamed']:
if log.action == 'osf_storage_folder_created':
return OsfStorageFolder.objects.get(
target_object_id=reg.registered_from.id,
name=log.params['path'].split('/')[-2]
)
elif log.action == 'osf_storage_file_removed':
path = log.params['path'].split('/')
return TrashedFileNode.objects.get(
target_object_id=reg.registered_from.id,
name=path[-1] or path[-2] # file name or folder name
)
elif log.action in ['addon_file_moved', 'addon_file_renamed']:
try:
return BaseFileNode.objects.get(_id=log.params['source']['path'].rstrip('/').split('/')[-1])
except (KeyError, BaseFileNode.DoesNotExist):
return BaseFileNode.objects.get(_id=log.params['destination']['path'].rstrip('/').split('/')[-1])
elif log.action == 'osf_storage_file_removed':
candidates = BaseFileNode.objects.filter(
target_object_id=reg.registered_from.id,
target_content_type_id=ContentType.objects.get_for_model(AbstractNode).id,
name=path[-1] or path[-2],
deleted_on__lte=log.date
).order_by('-deleted_on')
else:
# Generic fallback
candidates = BaseFileNode.objects.filter(
target_object_id=reg.registered_from.id,
target_content_type_id=ContentType.objects.get_for_model(AbstractNode).id,
name=path[-1] or path[-2],
created__lte=log.date
).order_by('-created')

if candidates.exists():
return candidates.first()
path = log.params.get('path', '').split('/')
if len(path) >= 2:
name = path[-1] or path[-2] # file name or folder name
return BaseFileNode.objects.get(
target_object_id=reg.registered_from.id,
name=name
)

raise BaseFileNode.DoesNotExist(f"No file found for name '{path[-1] or path[-2]}' before {log.date}")
raise ValueError(f'Cannot determine file obj for log {log._id} [Registration id {reg._id}]: {log.action}')


def handle_file_operation(file_tree, reg, file_obj, log, obj_cache):
Expand Down
18 changes: 18 additions & 0 deletions osf/migrations/0032_brand_background_color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2025-08-12 12:48

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('osf', '0031_abstractprovider_registration_word'),
]

operations = [
migrations.AddField(
model_name='brand',
name='background_color',
field=models.CharField(blank=True, max_length=7, null=True),
),
]
1 change: 1 addition & 0 deletions osf/models/brand.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Meta:

primary_color = models.CharField(max_length=7)
secondary_color = models.CharField(max_length=7)
background_color = models.CharField(max_length=7, blank=True, null=True)

def __str__(self):
return f'{self.name} ({self.id})'
1 change: 1 addition & 0 deletions osf_tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,7 @@ class Meta:

primary_color = factory.Faker('hex_color')
secondary_color = factory.Faker('hex_color')
background_color = factory.Faker('hex_color')


class SchemaResponseFactory(DjangoModelFactory):
Expand Down
43 changes: 0 additions & 43 deletions osf_tests/management_commands/test_force_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,49 +102,6 @@ def test_generic_fallback(self, node, reg):
file_obj = get_file_obj_from_log(log, reg)
assert file_obj == file

@pytest.mark.django_db
def test_file_multiple_creations_deletions(self, node, reg):
file1 = OsfStorageFile.create(target=node, name='duplicate.txt')
file1.save()
file1.delete()
log1 = NodeLog.objects.create(
node=node,
action='osf_storage_file_removed',
params={'path': '/duplicate.txt'},
date=timezone.now(),
)

file2 = OsfStorageFile.create(target=node, name='duplicate.txt')
file2.save()
file2.delete()
log2 = NodeLog.objects.create(
node=node,
action='osf_storage_file_removed',
params={'path': '/duplicate.txt'},
date=timezone.now(),
)

file3 = OsfStorageFile.create(target=node, name='duplicate.txt')
file3.save()
log3 = NodeLog.objects.create(
node=node,
action='osf_storage_file_added',
params={'urls': {'view': f'/{node._id}/files/osfstorage/{file3._id}/'}},
date=timezone.now(),
)

file_obj1 = get_file_obj_from_log(log1, reg)
assert file_obj1 == file1
assert isinstance(file_obj1, TrashedFileNode)

file_obj2 = get_file_obj_from_log(log2, reg)
assert file_obj2 == file2
assert isinstance(file_obj2, TrashedFileNode)

file_obj3 = get_file_obj_from_log(log3, reg)
assert file_obj3 == file3
assert isinstance(file_obj3, OsfStorageFile)


class TestBuildFileTree:

Expand Down
Loading