From e5b19d4b9490e822a2403e63c25e990599aeca08 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Wed, 17 Sep 2025 12:02:57 -0400 Subject: [PATCH 01/10] Merge Feature/pbs-25-16 into develop (#11306) * [ENG-8514] Remove CSRF protection from reset password api v2 POST (#11246) * remove csrf protection from reset password endpoint * update test * add background color prop to Brand (#11254) * fix flaky test_serialized_metadata * fix throttle test * reset throttle cache * don't ignore components when create a view-only link * fixed updating subscribe_osf_general_email subscription * [ENG-7277] Update doc to include version as relationship (#11262) * added tests * [ENG-8691] Wrong server on reset password email (#11271) * use same logic for admin and web password reset * fix tests * enable filtering for linked-nodes endpoint (#11273) * remove deleted users from institutional dashboard (#11261) * fixed tests * [ENG-7803] Update text on VOL modal for registration (#11279) * added description filtering for preprints (#11294) ## Purpose User should be able to filter preprints by their description ## Changes Added description to filterable fields, added tests ## Ticket https://openscience.atlassian.net/browse/ENG-8921?atlOrigin=eyJpIjoiNmY1MDUwZjI0Mjg2NDc2OGI2N2U1ZDQ0MjIwNzU0OTAiLCJwIjoiaiJ9 * [ENG-8689] - Fix/eng 8689 (#11267) * Fix color picker for brands * Remove unnecessary exclusion * fix: file dataset metadata download (#11296) * fix linting * merge migrations * [ENG-8958] missing metadata (dcterms:type and osf:hasOsfAddon) (#11309) * fix: validate resource_type_general in api * fix: update search after updating addons * fix: file dataset metadata with mononym creators (#11297) * Revert "fix: file dataset metadata with mononym creators (#11297)" (#11311) This reverts commit 21090b2870639ef317c22cb83590c181f6a18dd1. --------- Co-authored-by: antkryt Co-authored-by: John Tordoff Co-authored-by: Ihor Sokhan Co-authored-by: Vlad0n20 <137097005+Vlad0n20@users.noreply.github.com> Co-authored-by: abram axel booth --- admin/brands/forms.py | 1 + admin/brands/views.py | 16 ++++ admin/static/js/banners/banners.js | 5 +- admin/static/js/brands/brands.js | 5 +- admin/users/views.py | 16 +--- api/brands/serializers.py | 1 + api/collections/views.py | 8 +- api/custom_metadata/serializers.py | 5 +- api/institutions/views.py | 1 + api/nodes/serializers.py | 5 +- api/preprints/serializers.py | 48 +++++++++-- api/preprints/views.py | 10 +++ api/users/serializers.py | 2 +- api/users/services.py | 27 ++++++ api/users/views.py | 42 ++++------ .../test_custom_item_metadata.py | 13 +++ .../views/test_node_view_only_links_list.py | 82 ++++++++++++++++++- api_tests/preprints/filters/test_filters.py | 27 +++++- .../test_registration_view_only_links_list.py | 5 ++ api_tests/users/views/test_user_settings.py | 46 +++++------ .../users/views/test_user_settings_detail.py | 5 +- osf/metadata/definitions/datacite/__init__.py | 39 +++++++++ osf/metadata/osf_gathering.py | 35 +------- .../serializers/google_dataset_json_ld.py | 8 +- osf/migrations/0032_brand_background_color.py | 18 ++++ osf/migrations/0033_merge_20250915_2100.py | 14 ++++ osf/models/brand.py | 1 + osf/models/mixins.py | 2 +- osf/tasks.py | 2 + osf_tests/factories.py | 1 + .../file_basic.google-dataset.json | 19 +++++ .../expected_metadata_files/file_basic.turtle | 2 + .../file_full.google-dataset.json | 19 +++++ .../expected_metadata_files/file_full.turtle | 2 + .../preprint_basic.google-dataset.json | 27 ++++++ .../preprint_basic.turtle | 2 + .../preprint_full.google-dataset.json | 27 ++++++ .../preprint_full.turtle | 2 + .../project_basic.datacite.json | 2 +- .../project_basic.datacite.xml | 2 +- .../project_basic.google-dataset.json | 35 ++++++++ .../project_basic.turtle | 4 +- .../project_full.datacite.json | 2 +- .../project_full.datacite.xml | 2 +- .../project_full.google-dataset.json | 37 +++++++++ .../project_full.turtle | 4 +- .../registration_basic.datacite.json | 2 +- .../registration_basic.datacite.xml | 2 +- .../registration_basic.google-dataset.json | 41 ++++++++++ .../registration_basic.turtle | 4 +- .../registration_full.datacite.json | 2 +- .../registration_full.datacite.xml | 2 +- .../registration_full.google-dataset.json | 43 ++++++++++ .../registration_full.turtle | 4 +- .../expected_metadata_files/user_basic.turtle | 2 + .../expected_metadata_files/user_full.turtle | 2 + osf_tests/metadata/test_google_datasets.py | 25 ------ osf_tests/metadata/test_osf_gathering.py | 6 ++ .../metadata/test_serialized_metadata.py | 12 ++- osf_tests/test_gv_addon_logs.py | 4 + .../project/modal_generate_private_link.mako | 4 +- 61 files changed, 677 insertions(+), 156 deletions(-) create mode 100644 api/users/services.py create mode 100644 osf/metadata/definitions/datacite/__init__.py create mode 100644 osf/migrations/0032_brand_background_color.py create mode 100644 osf/migrations/0033_merge_20250915_2100.py create mode 100644 osf_tests/metadata/expected_metadata_files/file_basic.google-dataset.json create mode 100644 osf_tests/metadata/expected_metadata_files/file_full.google-dataset.json create mode 100644 osf_tests/metadata/expected_metadata_files/preprint_basic.google-dataset.json create mode 100644 osf_tests/metadata/expected_metadata_files/preprint_full.google-dataset.json create mode 100644 osf_tests/metadata/expected_metadata_files/project_basic.google-dataset.json create mode 100644 osf_tests/metadata/expected_metadata_files/project_full.google-dataset.json create mode 100644 osf_tests/metadata/expected_metadata_files/registration_basic.google-dataset.json create mode 100644 osf_tests/metadata/expected_metadata_files/registration_full.google-dataset.json delete mode 100644 osf_tests/metadata/test_google_datasets.py diff --git a/admin/brands/forms.py b/admin/brands/forms.py index 942d4929338..05693c66a05 100644 --- a/admin/brands/forms.py +++ b/admin/brands/forms.py @@ -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} diff --git a/admin/brands/views.py b/admin/brands/views.py index 01b449d3fe8..82d8a07090a 100644 --- a/admin/brands/views.py +++ b/admin/brands/views.py @@ -60,6 +60,14 @@ class BrandChangeForm(PermissionRequiredMixin, UpdateView): raise_exception = True model = Brand form_class = BrandForm + template_name = 'brands/detail.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['change_form'] = context.get('form') + brand_obj = self.get_object() + context['brand'] = model_to_dict(brand_obj) + return context def get_object(self, queryset=None): brand_id = self.kwargs.get('brand_id') @@ -81,6 +89,7 @@ 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. @@ -88,6 +97,9 @@ def post(self, request, *args, **kwargs): 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) @@ -109,6 +121,7 @@ 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. @@ -116,4 +129,7 @@ def post(self, request, *args, **kwargs): 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) diff --git a/admin/static/js/banners/banners.js b/admin/static/js/banners/banners.js index c39046000a7..e9d8b7d87e8 100644 --- a/admin/static/js/banners/banners.js +++ b/admin/static/js/banners/banners.js @@ -26,6 +26,9 @@ $(document).ready(function() { } }); - $(".colorpicker").colorpicker(); + $(".colorpicker").colorpicker({ + format: 'hex', + useAlpha: false + }); }); diff --git a/admin/static/js/brands/brands.js b/admin/static/js/brands/brands.js index 1115261d372..1acdca7f21b 100644 --- a/admin/static/js/brands/brands.js +++ b/admin/static/js/brands/brands.js @@ -5,6 +5,9 @@ require('bootstrap-colorpicker/dist/css/bootstrap-colorpicker.min.css'); $(document).ready(function() { - $(".colorpicker").colorpicker(); + $(".colorpicker").colorpicker({ + format: 'hex', + useAlpha: false + }); }); diff --git a/admin/users/views.py b/admin/users/views.py index 38787a84a23..48c77a2bd26 100644 --- a/admin/users/views.py +++ b/admin/users/views.py @@ -12,7 +12,6 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.urls import reverse from django.core.exceptions import PermissionDenied -from django.core.mail import send_mail from django.shortcuts import redirect from django.core.paginator import Paginator from django.core.exceptions import ValidationError @@ -47,7 +46,8 @@ AddSystemTagForm ) from admin.base.views import GuidView -from website.settings import DOMAIN, OSF_SUPPORT_EMAIL +from api.users.services import send_password_reset_email +from website.settings import DOMAIN from django.urls import reverse_lazy @@ -523,17 +523,9 @@ class ResetPasswordView(UserMixin, View): def post(self, request, *args, **kwargs): email = self.request.POST['emails'] user = get_user(email) - url = furl(DOMAIN) - user.verification_key_v2 = generate_verification_key(verification_type='password_admin') - user.save() - url.add(path=f'resetpassword/{user._id}/{user.verification_key_v2["token"]}') - send_mail( - subject='Reset OSF Password', - message=f'Follow this link to reset your password: {url.url}\n Note: this link will expire in 12 hours', - from_email=OSF_SUPPORT_EMAIL, - recipient_list=[email] - ) + send_password_reset_email(user, email, institutional=False, verification_type='password_admin') + update_admin_log( user_id=self.request.user.id, object_id=user.pk, diff --git a/api/brands/serializers.py b/api/brands/serializers.py index 8d040e7a93d..485d515b098 100644 --- a/api/brands/serializers.py +++ b/api/brands/serializers.py @@ -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', diff --git a/api/collections/views.py b/api/collections/views.py index 41c6654e7ea..3680b0cb04a 100644 --- a/api/collections/views.py +++ b/api/collections/views.py @@ -36,6 +36,7 @@ CollectedRegistrationsRelationshipSerializer, ) from api.nodes.serializers import NodeSerializer +from api.nodes.filters import NodesFilterMixin from api.preprints.serializers import PreprintSerializer from api.subjects.views import SubjectRelationshipBaseView, BaseResourceSubjectsList from api.registrations.serializers import RegistrationSerializer @@ -506,7 +507,7 @@ def get_resource(self, check_object_permissions=True): return self.get_collection_submission(check_object_permissions) -class LinkedNodesList(JSONAPIBaseView, generics.ListAPIView, CollectionMixin, NodeOptimizationMixin): +class LinkedNodesList(JSONAPIBaseView, generics.ListAPIView, CollectionMixin, NodeOptimizationMixin, NodesFilterMixin): """List of nodes linked to this node. *Read-only*. Linked nodes are the project/component nodes pointed to by node links. This view will probably replace node_links in the near future. @@ -569,12 +570,15 @@ class LinkedNodesList(JSONAPIBaseView, generics.ListAPIView, CollectionMixin, No ordering = ('-modified',) - def get_queryset(self): + def get_default_queryset(self): auth = get_user_auth(self.request) node_ids = self.get_collection().active_guids.filter(content_type_id=ContentType.objects.get_for_model(Node).id).values_list('object_id', flat=True) nodes = Node.objects.filter(id__in=node_ids, is_deleted=False).can_view(user=auth.user, private_link=auth.private_link).order_by('-modified') return self.optimize_node_queryset(nodes) + def get_queryset(self): + return self.get_queryset_from_request() + # overrides APIView def get_parser_context(self, http_request): """ diff --git a/api/custom_metadata/serializers.py b/api/custom_metadata/serializers.py index bc54c2cbb40..0b1fc31eac6 100644 --- a/api/custom_metadata/serializers.py +++ b/api/custom_metadata/serializers.py @@ -2,6 +2,7 @@ import rest_framework.serializers as ser from framework.auth.core import Auth +from osf.metadata.definitions.datacite import DATACITE_RESOURCE_TYPES_GENERAL from api.base.serializers import ( IDField, JSONAPISerializer, @@ -80,10 +81,10 @@ class CustomItemMetadataSerializer(JSONAPISerializer): allow_blank=True, max_length=REASONABLE_MAX_LENGTH, ) - resource_type_general = ser.CharField( + resource_type_general = ser.ChoiceField( required=False, allow_blank=True, - max_length=REASONABLE_MAX_LENGTH, + choices=sorted(DATACITE_RESOURCE_TYPES_GENERAL), ) funders = FundingInfoSerializer( many=True, diff --git a/api/institutions/views.py b/api/institutions/views.py index b7b43d0e718..9424a69fa10 100644 --- a/api/institutions/views.py +++ b/api/institutions/views.py @@ -592,6 +592,7 @@ def get_default_search(self): InstitutionalUserReport.search() .filter('term', report_yearmonth=str(_yearmonth)) .filter('term', institution_id=self.get_institution()._id) + .exclude('term', user_name='Deleted user') ) diff --git a/api/nodes/serializers.py b/api/nodes/serializers.py index f0b787a9bc5..f3323d356cb 100644 --- a/api/nodes/serializers.py +++ b/api/nodes/serializers.py @@ -1689,12 +1689,15 @@ def create(self, validated_data): user = get_user_auth(self.context['request']).user anonymous = validated_data.pop('anonymous') node = self.context['view'].get_node() + children = self.context['request'].data.pop('target_type', []) + if children: + children = [AbstractNode.load(id) for id in children if id != 'nodes'] try: view_only_link = new_private_link( name=name, user=user, - nodes=[node], + nodes=[node, *children], anonymous=anonymous, ) except ValidationError: diff --git a/api/preprints/serializers.py b/api/preprints/serializers.py index 309d0b473a3..ee109f852a6 100644 --- a/api/preprints/serializers.py +++ b/api/preprints/serializers.py @@ -103,6 +103,7 @@ class PreprintSerializer(TaxonomizableSerializerMixin, MetricsSerializerMixin, J 'reviews_state', 'node_is_public', 'tags', + 'description', ]) available_metrics = frozenset([ 'downloads', @@ -154,6 +155,10 @@ class PreprintSerializer(TaxonomizableSerializerMixin, MetricsSerializerMixin, J related_view='preprints:preprint-versions', related_view_kwargs={'preprint_id': '<_id>'}, read_only=True, + help_text=( + 'Relationship to all versions of this preprint. ' + 'Related URL: /v2/preprints/{preprint_id}/versions/ (GET to list, POST to create a new version).' + ), ) citation = NoneIfWithdrawal( @@ -190,6 +195,7 @@ class PreprintSerializer(TaxonomizableSerializerMixin, MetricsSerializerMixin, J related_view='providers:preprint-providers:preprint-provider-detail', related_view_kwargs={'provider_id': ''}, read_only=False, + help_text='Relationship to the preprint provider. Required on creation.', ) files = NoneIfWithdrawal( @@ -500,11 +506,30 @@ class Meta: class PreprintCreateSerializer(PreprintSerializer): + """Serializer for creating a new preprint. + + Notes + - Overrides `PreprintSerializer` to allow nullable `id` and implements `create`. + - Requires `provider` and `title`. + - Optional `description`. + - Optional privileged fields: `manual_guid`, `manual_doi` (gated by MANUAL_DOI_AND_GUID flag). + """ # Overrides PreprintSerializer to make id nullable, adds `create` - # TODO: add better Docstrings id = IDField(source='_id', required=False, allow_null=True) - manual_guid = ser.CharField(write_only=True, required=False, allow_null=True, allow_blank=True) - manual_doi = ser.CharField(write_only=True, required=False, allow_null=True, allow_blank=True) + manual_guid = ser.CharField( + write_only=True, + required=False, + allow_null=True, + allow_blank=True, + help_text='Privileged: manually assign a GUID on creation (feature-flag gated).', + ) + manual_doi = ser.CharField( + write_only=True, + required=False, + allow_null=True, + allow_blank=True, + help_text='Privileged: manually assign an article DOI on creation (feature-flag gated).', + ) def create(self, validated_data): creator = self.context['request'].user @@ -526,11 +551,22 @@ def create(self, validated_data): class PreprintCreateVersionSerializer(PreprintSerializer): - # Overrides PreprintSerializer to make title nullable and customize version creation - # TODO: add better Docstrings + """Serializer for creating a new version of an existing preprint. + + Notes + - Overrides `PreprintSerializer` to make `title` optional during version creation. + - Requires `create_from_guid` referencing the source preprint GUID (base or versioned). + - Only users with ADMIN on the source preprint may create a new version. + """ id = IDField(source='_id', required=False, allow_null=True) title = ser.CharField(required=False) - create_from_guid = ser.CharField(required=True, write_only=True) + create_from_guid = ser.CharField( + required=True, + write_only=True, + help_text=( + 'GUID of the source preprint to version (accepts base GUID or versioned GUID, e.g., abc12 or abc12_v3).' + ), + ) def create(self, validated_data): create_from_guid = validated_data.pop('create_from_guid', None) diff --git a/api/preprints/views.py b/api/preprints/views.py index ae5f2d1f11a..147ca2faaab 100644 --- a/api/preprints/views.py +++ b/api/preprints/views.py @@ -230,6 +230,13 @@ def get_annotated_queryset_with_metrics(self, queryset, metric_class, metric_nam class PreprintVersionsList(PreprintMetricsViewMixin, JSONAPIBaseView, generics.ListCreateAPIView, PreprintFilterMixin): + """List existing versions of a preprint or create a new version. + + GET: Returns a collection of preprint resources representing all versions of the given preprint. + POST: Creates a new version from the current preprint. Requires ADMIN on the source preprint. + + Related to the `versions` relationship on the Preprint resource. + """ # These permissions are not checked for the list of preprints, permissions handled by the query permission_classes = ( drf_permissions.IsAuthenticatedOrReadOnly, @@ -284,6 +291,9 @@ def create(self, request, *args, **kwargs): class PreprintDetail(PreprintOldVersionsImmutableMixin, PreprintMetricsViewMixin, JSONAPIBaseView, generics.RetrieveUpdateDestroyAPIView, PreprintMixin, WaterButlerMixin): """The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/preprints_read). + + Note: The resource now exposes a `versions` relationship pointing to + `/v2/preprints/{preprint_id}/versions/` for listing or creating versions. """ permission_classes = ( drf_permissions.IsAuthenticatedOrReadOnly, diff --git a/api/users/serializers.py b/api/users/serializers.py index 8c5d0bc7469..87216dc147b 100644 --- a/api/users/serializers.py +++ b/api/users/serializers.py @@ -574,6 +574,7 @@ def update_email_preferences(self, instance, attr, value): instance._id, username=instance.username, ) + instance.reload() else: raise exceptions.ValidationError(detail='Invalid email preference.') @@ -617,7 +618,6 @@ def to_representation(self, instance): return UserSettingsSerializer(instance=instance, context=context).data def update(self, instance, validated_data): - for attr, value in validated_data.items(): if 'two_factor_enabled' == attr: two_factor_addon = instance.get_addon('twofactor') diff --git a/api/users/services.py b/api/users/services.py new file mode 100644 index 00000000000..3af86c0807c --- /dev/null +++ b/api/users/services.py @@ -0,0 +1,27 @@ +from furl import furl +from django.utils import timezone + +from framework.auth.core import generate_verification_key +from website import settings, mails + + +def send_password_reset_email(user, email, verification_type='password', institutional=False, **mail_kwargs): + """Generate a password reset token, save it to the user and send the password reset email. + """ + # new verification key (v2) + user.verification_key_v2 = generate_verification_key(verification_type=verification_type) + user.email_last_sent = timezone.now() + user.save() + + reset_link = furl(settings.DOMAIN).add(path=f'resetpassword/{user._id}/{user.verification_key_v2["token"]}').url + mail_template = mails.FORGOT_PASSWORD if not institutional else mails.FORGOT_PASSWORD_INSTITUTION + + mails.send_mail( + to_addr=email, + mail=mail_template, + reset_link=reset_link, + can_change_preferences=False, + **mail_kwargs, + ) + + return reset_link diff --git a/api/users/views.py b/api/users/views.py index 8dea51613df..a35a5e9511f 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -45,6 +45,7 @@ from api.registrations.serializers import RegistrationSerializer from api.resources import annotations as resource_annotations +from api.users.services import send_password_reset_email from api.users.permissions import ( CurrentUser, ReadOnlyOrCurrentUser, ReadOnlyOrCurrentUserRelationship, @@ -817,40 +818,31 @@ class ResetPassword(JSONAPIBaseView, generics.ListCreateAPIView): throttle_classes = (NonCookieAuthThrottle, BurstRateThrottle, RootAnonThrottle, SendEmailThrottle) def get(self, request, *args, **kwargs): + institutional = bool(request.query_params.get('institutional', None)) email = request.query_params.get('email', None) if not email: raise ValidationError('Request must include email in query params.') - institutional = bool(request.query_params.get('institutional', None)) - mail_template = mails.FORGOT_PASSWORD if not institutional else mails.FORGOT_PASSWORD_INSTITUTION - - status_message = language.RESET_PASSWORD_SUCCESS_STATUS_MESSAGE.format(email=email) - kind = 'success' # check if the user exists user_obj = get_user(email=email) - - if user_obj: + if user_obj and user_obj.is_active: # rate limit forgot_password_post if not throttle_period_expired(user_obj.email_last_sent, settings.SEND_EMAIL_THROTTLE): - status_message = 'You have recently requested to change your password. Please wait a few minutes ' \ - 'before trying again.' - kind = 'error' - return Response({'message': status_message, 'kind': kind}, status=status.HTTP_429_TOO_MANY_REQUESTS) - elif user_obj.is_active: - # new random verification key (v2) - user_obj.verification_key_v2 = generate_verification_key(verification_type='password') - user_obj.email_last_sent = timezone.now() - user_obj.save() - reset_link = f'{settings.RESET_PASSWORD_URL}{user_obj._id}/{user_obj.verification_key_v2['token']}/' - mails.send_mail( - to_addr=email, - mail=mail_template, - reset_link=reset_link, - can_change_preferences=False, - ) - return Response(status=status.HTTP_200_OK, data={'message': status_message, 'kind': kind, 'institutional': institutional}) + status_message = 'You have recently requested to change your password. ' \ + 'Please wait a few minutes before trying again.' + return Response({'message': status_message, 'kind': 'error'}, status=status.HTTP_429_TOO_MANY_REQUESTS) + + send_password_reset_email(user_obj, email, institutional=institutional) + + return Response( + status=status.HTTP_200_OK, + data={ + 'message': language.RESET_PASSWORD_SUCCESS_STATUS_MESSAGE.format(email=email), + 'kind': 'success', + '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) diff --git a/api_tests/metadata_records/test_custom_item_metadata.py b/api_tests/metadata_records/test_custom_item_metadata.py index 690ff0ea22e..4de6de457ab 100644 --- a/api_tests/metadata_records/test_custom_item_metadata.py +++ b/api_tests/metadata_records/test_custom_item_metadata.py @@ -342,6 +342,19 @@ def test_with_write_permission(self, app, public_osfguid, private_osfguid, anybo }, ) + # cannot PUT invalid resource_type_general + res = app.put_json_api( + self.make_url(osfguid), + self.make_payload( + osfguid, + language='en', + resource_type_general='book-chapter', + ), + auth=anybody_with_write_permission.auth, + ) + assert res.status_code == 400 + expected.assert_expectations(db_record=db_record, api_record=None) # db unchanged + # can PATCH expected.language = 'nga-CD' res = app.patch_json_api( diff --git a/api_tests/nodes/views/test_node_view_only_links_list.py b/api_tests/nodes/views/test_node_view_only_links_list.py index 1bf37bddc61..7dd44e4b5c5 100644 --- a/api_tests/nodes/views/test_node_view_only_links_list.py +++ b/api_tests/nodes/views/test_node_view_only_links_list.py @@ -5,7 +5,8 @@ from osf_tests.factories import ( ProjectFactory, AuthUserFactory, - PrivateLinkFactory + PrivateLinkFactory, + NodeRelationFactory ) @@ -54,6 +55,11 @@ def view_only_link(public_project): return view_only_link +@pytest.fixture() +def components_factory(): + return ProjectFactory + + @pytest.mark.django_db class TestViewOnlyLinksList: @@ -227,3 +233,77 @@ def test_cannot_create_vol( } res = app.post_json_api(url, {'data': payload}, expect_errors=True) assert res.status_code == 401 + + def test_create_vol_with_components( + self, app, user, url, public_project, view_only_link, components_factory): + component1 = components_factory(creator=user) + component2 = components_factory(creator=user) + component3 = components_factory(creator=user) + NodeRelationFactory(parent=public_project, child=component1) + NodeRelationFactory(parent=public_project, child=component2) + NodeRelationFactory(parent=public_project, child=component3) + + url = f'{url}?embed=creator&embed=nodes' + payload = { + 'attributes': { + 'name': 'testmultiplenodes', + 'anonymous': False, + }, + 'relationships': { + 'nodes': { + 'data': [ + { + 'id': component1._id, + 'type': 'nodes' + }, + { + 'id': component2._id, + 'type': 'nodes' + }, + { + 'id': component3._id, + 'type': 'nodes' + } + ] + } + } + } + res = app.post_json_api(url, {'data': payload}, auth=user.auth) + assert res.status_code == 201 + data = res.json['data'] + assert len(data['embeds']['nodes']['data']) == 4 + + def test_create_vol_no_duplicated_components( + self, app, user, url, public_project, view_only_link, components_factory): + component1 = components_factory(creator=user) + NodeRelationFactory(parent=public_project, child=component1) + + url = f'{url}?embed=creator&embed=nodes' + payload = { + 'attributes': { + 'name': 'testmultiplenodes', + 'anonymous': False, + }, + 'relationships': { + 'nodes': { + 'data': [ + { + 'id': component1._id, + 'type': 'nodes' + }, + { + 'id': component1._id, + 'type': 'nodes' + }, + { + 'id': component1._id, + 'type': 'nodes' + }, + ] + } + } + } + res = app.post_json_api(url, {'data': payload}, auth=user.auth) + assert res.status_code == 201 + data = res.json['data'] + assert len(data['embeds']['nodes']['data']) == 2 diff --git a/api_tests/preprints/filters/test_filters.py b/api_tests/preprints/filters/test_filters.py index 9a483ad55ec..6f1a1e6c0af 100644 --- a/api_tests/preprints/filters/test_filters.py +++ b/api_tests/preprints/filters/test_filters.py @@ -56,7 +56,9 @@ def preprint_one(self, user, project_one, provider_one, subject_one): creator=user, project=project_one, provider=provider_one, - subjects=[[subject_one._id]]) + subjects=[[subject_one._id]], + description='test1' + ) preprint_one.original_publication_date = '2013-12-25 10:09:08.070605+00:00' preprint_one.save() return preprint_one @@ -68,7 +70,9 @@ def preprint_two(self, user, project_two, provider_two, subject_two): project=project_two, filename='howto_reason.txt', provider=provider_two, - subjects=[[subject_two._id]]) + subjects=[[subject_two._id]], + description='2test' + ) preprint_two.created = '2013-12-11 10:09:08.070605+00:00' preprint_two.date_published = '2013-12-11 10:09:08.070605+00:00' preprint_two.original_publication_date = '2013-12-11 10:09:08.070605+00:00' @@ -84,7 +88,9 @@ def preprint_three( project=project_three, filename='darn_reason.txt', provider=provider_three, - subjects=[[subject_one._id], [subject_two._id]]) + subjects=[[subject_one._id], [subject_two._id]], + description='new preprint' + ) preprint_three.created = '2013-12-11 10:09:08.070605+00:00' preprint_three.date_published = '2013-12-11 10:09:08.070605+00:00' preprint_three.original_publication_date = '2013-12-11 10:09:08.070605+00:00' @@ -129,6 +135,10 @@ def is_published_and_modified_url(self, url): def node_is_public_url(self, url): return f'{url}filter[node_is_public]=' + @pytest.fixture() + def description_url(self, url): + return f'{url}filter[description]=' + def test_provider_filter_null(self, app, user, provider_url): expected = [] res = app.get(f'{provider_url}null', auth=user.auth) @@ -351,3 +361,14 @@ def actual(): expected.update( [p._id for p in preprints if p.provider_id == preprint.provider_id]) assert expected == actual() + + def test_description_filter( + self, app, user, description_url, preprint_one, preprint_two, preprint_three): + expected = {preprint_one._id, preprint_two._id} + res = app.get( + description_url + 'tes', + auth=user.auth + ) + actual = {preprint['id'] for preprint in res.json['data']} + assert expected == actual + assert preprint_three._id not in res.json['data'] diff --git a/api_tests/registrations/views/test_registration_view_only_links_list.py b/api_tests/registrations/views/test_registration_view_only_links_list.py index bb87711327e..7946c4fd44f 100644 --- a/api_tests/registrations/views/test_registration_view_only_links_list.py +++ b/api_tests/registrations/views/test_registration_view_only_links_list.py @@ -58,6 +58,11 @@ def view_only_link(public_project): return view_only_link +@pytest.fixture +def components_factory(): + return RegistrationFactory + + class TestRegistrationViewOnlyLinksList(TestViewOnlyLinksList): pass diff --git a/api_tests/users/views/test_user_settings.py b/api_tests/users/views/test_user_settings.py index 48c5ca75687..decaed93712 100644 --- a/api_tests/users/views/test_user_settings.py +++ b/api_tests/users/views/test_user_settings.py @@ -9,10 +9,11 @@ AuthUserFactory, UserFactory, ) -from website import settings from django.middleware import csrf +from django.core.cache import cache from osf.models import Email, NotableDomain from framework.auth.views import auth_email_logout +from website import mails, settings @pytest.fixture() def user_one(): @@ -189,14 +190,24 @@ def url(self): def csrf_token(self): return csrf._mask_cipher_secret(csrf._get_new_csrf_string()) - def test_get(self, mock_send_grid, app, url, user_one): + @pytest.fixture(autouse=True) + def clear_throttle_cache(self): + cache.clear() + + def test_get(self, app, url, user_one): encoded_email = urllib.parse.quote(user_one.email) url = f'{url}?email={encoded_email}' - res = app.get(url) - assert res.status_code == 200 + with mock.patch.object(mails, 'send_mail', return_value=None) as mock_send_mail: + res = app.get(url) + assert res.status_code == 200 - user_one.reload() - assert mock_send_grid.call_args[1]['to_addr'] == user_one.username + user_one.reload() + mock_send_mail.assert_called_with( + to_addr=user_one.username, + mail=mails.FORGOT_PASSWORD, + reset_link=f'{settings.DOMAIN}resetpassword/{user_one._id}/{user_one.verification_key_v2['token']}', + can_change_preferences=False, + ) def test_get_invalid_email(self, mock_send_grid, app, url): url = f'{url}?email={'invalid_email'}' @@ -204,8 +215,7 @@ def test_get_invalid_email(self, mock_send_grid, app, url): assert res.status_code == 200 assert not mock_send_grid.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) @@ -220,7 +230,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') @@ -269,7 +279,7 @@ def test_post_invalid_password(self, app, url, user_one, csrf_token): res = app.post_json_api(url, payload, expect_errors=True, headers={'X-THROTTLE-TOKEN': 'test-token', 'X-CSRFToken': csrf_token}) assert res.status_code == 400 - def test_throttle(self, app, url, user_one, csrf_token): + def test_throttle(self, app, url, user_one): encoded_email = urllib.parse.quote(user_one.email) url = f'{url}?email={encoded_email}' app.get(url) @@ -284,21 +294,11 @@ def test_throttle(self, app, url, user_one, csrf_token): } } - res = app.post_json_api( - url, - payload, - headers={'X-CSRFToken': csrf_token}, - expect_errors=True - ) - res = app.post_json_api( - url, - payload, - headers={'X-CSRFToken': csrf_token}, - expect_errors=True - ) - assert res.status_code == 429 + res = app.post_json_api(url, payload, expect_errors=False) + assert res.status_code == 200 res = app.get(url, expect_errors=True) + assert res.status_code == 429 assert res.json['message'] == 'You have recently requested to change your password. Please wait a few minutes before trying again.' diff --git a/api_tests/users/views/test_user_settings_detail.py b/api_tests/users/views/test_user_settings_detail.py index fe4e5acb2fc..821ee82616a 100644 --- a/api_tests/users/views/test_user_settings_detail.py +++ b/api_tests/users/views/test_user_settings_detail.py @@ -4,7 +4,7 @@ from osf_tests.factories import ( AuthUserFactory, ) -from website.settings import OSF_HELP_LIST +from website.settings import OSF_HELP_LIST, MAILCHIMP_GENERAL_LIST @pytest.fixture() @@ -208,6 +208,9 @@ def test_authorized_patch_200(self, mock_mailchimp_client, app, user_one, payloa user_one.refresh_from_db() assert res.json['data']['attributes']['subscribe_osf_help_email'] is False assert user_one.osf_mailing_lists[OSF_HELP_LIST] is False + + assert res.json['data']['attributes']['subscribe_osf_general_email'] is True + assert user_one.mailchimp_mailing_lists[MAILCHIMP_GENERAL_LIST] is True mock_mailchimp_client.assert_called_with() def test_bad_payload_patch_400(self, app, user_one, bad_payload, url): diff --git a/osf/metadata/definitions/datacite/__init__.py b/osf/metadata/definitions/datacite/__init__.py new file mode 100644 index 00000000000..9f0581588e2 --- /dev/null +++ b/osf/metadata/definitions/datacite/__init__.py @@ -0,0 +1,39 @@ +__all__ = ( + 'DATACITE_RESOURCE_TYPES_GENERAL', +) + +# controlled vocab for resourceTypeGeneral from https://schema.datacite.org/meta/kernel-4/ +DATACITE_RESOURCE_TYPES_GENERAL = frozenset(( + 'Audiovisual', + 'Award', + 'Book', + 'BookChapter', + 'Collection', + 'ComputationalNotebook', + 'ConferencePaper', + 'ConferenceProceeding', + 'DataPaper', + 'Dataset', + 'Dissertation', + 'Event', + 'Image', + 'Instrument', + 'InteractiveResource', + 'Journal', + 'JournalArticle', + 'Model', + 'OutputManagementPlan', + 'PeerReview', + 'PhysicalObject', + 'Preprint', + 'Project', + 'Report', + 'Service', + 'Software', + 'Sound', + 'Standard', + 'StudyRegistration', + 'Text', + 'Workflow', + 'Other', +)) diff --git a/osf/metadata/osf_gathering.py b/osf/metadata/osf_gathering.py index 0463de03dc1..91f3e0f7859 100644 --- a/osf/metadata/osf_gathering.py +++ b/osf/metadata/osf_gathering.py @@ -11,6 +11,7 @@ from api.caching.tasks import get_storage_usage_total from osf import models as osfdb from osf.metadata import gather +from osf.metadata.definitions.datacite import DATACITE_RESOURCE_TYPES_GENERAL from osf.metadata.rdfutils import ( DATACITE, DCAT, @@ -273,38 +274,6 @@ def pls_get_magic_metadata_basket(osf_item) -> gather.Basket: BEPRESS_SUBJECT_SCHEME_URI = 'https://bepress.com/reference_guide_dc/disciplines/' BEPRESS_SUBJECT_SCHEME_TITLE = 'bepress Digital Commons Three-Tiered Taxonomy' -DATACITE_RESOURCE_TYPES_GENERAL = { - 'Audiovisual', - 'Book', - 'BookChapter', - 'Collection', - 'ComputationalNotebook', - 'ConferencePaper', - 'ConferenceProceeding', - 'DataPaper', - 'Dataset', - 'Dissertation', - 'Event', - 'Image', - 'Instrument', - 'InteractiveResource', - 'Journal', - 'JournalArticle', - 'Model', - 'OutputManagementPlan', - 'PeerReview', - 'PhysicalObject', - 'Preprint', - 'Report', - 'Service', - 'Software', - 'Sound', - 'Standard', - 'StudyRegistration', - 'Text', - 'Workflow', - 'Other', -} DATACITE_RESOURCE_TYPE_BY_OSF_TYPE = { OSF.Preprint: 'Preprint', OSF.Registration: { @@ -1020,6 +989,8 @@ def gather_user_basics(focus): if isinstance(focus.dbmodel, osfdb.OSFUser): yield (RDF.type, FOAF.Person) # note: assumes osf user accounts represent people yield (FOAF.name, focus.dbmodel.fullname) + yield (FOAF.givenName, focus.dbmodel.given_name) + yield (FOAF.familyName, focus.dbmodel.family_name) _social_links = focus.dbmodel.social_links # special cases abound! do these one-by-one (based on OSFUser.SOCIAL_FIELDS) yield (DCTERMS.identifier, _social_links.get('github')) diff --git a/osf/metadata/serializers/google_dataset_json_ld.py b/osf/metadata/serializers/google_dataset_json_ld.py index 896e80acc7c..98c3dfd72ea 100644 --- a/osf/metadata/serializers/google_dataset_json_ld.py +++ b/osf/metadata/serializers/google_dataset_json_ld.py @@ -76,12 +76,12 @@ def metadata_as_dict(self) -> dict: def format_creators(basket): creator_data = [] - for creator in basket.focus.dbmodel.contributors.all(): + for creator_iri in basket[DCTERMS.creator]: creator_data.append({ '@type': 'Person', - 'name': creator.fullname, - 'givenName': creator.given_name, - 'familyName': creator.family_name + 'name': next(basket[creator_iri:FOAF.name]), + 'givenName': next(basket[creator_iri:FOAF.givenName]), + 'familyName': next(basket[creator_iri:FOAF.familyName]), }) return creator_data diff --git a/osf/migrations/0032_brand_background_color.py b/osf/migrations/0032_brand_background_color.py new file mode 100644 index 00000000000..9b465e81e4a --- /dev/null +++ b/osf/migrations/0032_brand_background_color.py @@ -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), + ), + ] diff --git a/osf/migrations/0033_merge_20250915_2100.py b/osf/migrations/0033_merge_20250915_2100.py new file mode 100644 index 00000000000..1a90663d741 --- /dev/null +++ b/osf/migrations/0033_merge_20250915_2100.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.15 on 2025-09-15 21:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0032_brand_background_color'), + ('osf', '0032_remove_osfgroup_creator_and_more'), + ] + + operations = [ + ] diff --git a/osf/models/brand.py b/osf/models/brand.py index ae4650407aa..5a857f5f3b1 100644 --- a/osf/models/brand.py +++ b/osf/models/brand.py @@ -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})' diff --git a/osf/models/mixins.py b/osf/models/mixins.py index da5351754d8..0e351edcd1b 100644 --- a/osf/models/mixins.py +++ b/osf/models/mixins.py @@ -138,7 +138,7 @@ def _complete_add_log(self, log, action, user=None, save=True): last_logged = log_date.replace(tzinfo=pytz.utc) else: recent_log = self.logs.latest('created') - log_date = recent_log.date if hasattr(log, 'date') else recent_log.created + log_date = recent_log.date if hasattr(recent_log, 'date') else recent_log.created last_logged = log_date if not log.should_hide: diff --git a/osf/tasks.py b/osf/tasks.py index 13854a63517..f30be62b09d 100644 --- a/osf/tasks.py +++ b/osf/tasks.py @@ -45,3 +45,5 @@ def log_gv_addon(node_url: str, action: str, user_url: str, addon: str): 'addon': addon } ) + if node.is_public: + node.update_search() diff --git a/osf_tests/factories.py b/osf_tests/factories.py index 1310c9aed63..4d60f1f19d3 100644 --- a/osf_tests/factories.py +++ b/osf_tests/factories.py @@ -1290,6 +1290,7 @@ class Meta: primary_color = factory.Faker('hex_color') secondary_color = factory.Faker('hex_color') + background_color = factory.Faker('hex_color') class SchemaResponseFactory(DjangoModelFactory): diff --git a/osf_tests/metadata/expected_metadata_files/file_basic.google-dataset.json b/osf_tests/metadata/expected_metadata_files/file_basic.google-dataset.json new file mode 100644 index 00000000000..8df3e277f07 --- /dev/null +++ b/osf_tests/metadata/expected_metadata_files/file_basic.google-dataset.json @@ -0,0 +1,19 @@ +{ + "@context": "https://schema.org", + "@type": "Dataset", + "creator": [], + "dateCreated": "2123-05-04", + "dateModified": "2123-05-04", + "description": "No description was included in this Dataset collected from the OSF", + "identifier": [ + "http://localhost:5000/w3ibb" + ], + "keywords": [], + "license": [], + "name": "my-file.blarg", + "publisher": { + "@type": "Organization", + "name": "Center For Open Science" + }, + "url": "http://localhost:5000/w3ibb" +} \ No newline at end of file diff --git a/osf_tests/metadata/expected_metadata_files/file_basic.turtle b/osf_tests/metadata/expected_metadata_files/file_basic.turtle index 3f430b22521..873b6a58875 100644 --- a/osf_tests/metadata/expected_metadata_files/file_basic.turtle +++ b/osf_tests/metadata/expected_metadata_files/file_basic.turtle @@ -41,6 +41,8 @@ a dcterms:Agent, foaf:Person ; dcterms:identifier "http://localhost:5000/w1ibb" ; + foaf:givenName "Person" ; + foaf:familyName "McNamington" ; foaf:name "Person McNamington" . a dcterms:Agent, diff --git a/osf_tests/metadata/expected_metadata_files/file_full.google-dataset.json b/osf_tests/metadata/expected_metadata_files/file_full.google-dataset.json new file mode 100644 index 00000000000..8df3e277f07 --- /dev/null +++ b/osf_tests/metadata/expected_metadata_files/file_full.google-dataset.json @@ -0,0 +1,19 @@ +{ + "@context": "https://schema.org", + "@type": "Dataset", + "creator": [], + "dateCreated": "2123-05-04", + "dateModified": "2123-05-04", + "description": "No description was included in this Dataset collected from the OSF", + "identifier": [ + "http://localhost:5000/w3ibb" + ], + "keywords": [], + "license": [], + "name": "my-file.blarg", + "publisher": { + "@type": "Organization", + "name": "Center For Open Science" + }, + "url": "http://localhost:5000/w3ibb" +} \ No newline at end of file diff --git a/osf_tests/metadata/expected_metadata_files/file_full.turtle b/osf_tests/metadata/expected_metadata_files/file_full.turtle index 175ccfb042f..492adf41375 100644 --- a/osf_tests/metadata/expected_metadata_files/file_full.turtle +++ b/osf_tests/metadata/expected_metadata_files/file_full.turtle @@ -70,6 +70,8 @@ a dcterms:Agent, foaf:Person ; dcterms:identifier "http://localhost:5000/w1ibb" ; + foaf:givenName "Person" ; + foaf:familyName "McNamington" ; foaf:name "Person McNamington" . a dcterms:Agent, diff --git a/osf_tests/metadata/expected_metadata_files/preprint_basic.google-dataset.json b/osf_tests/metadata/expected_metadata_files/preprint_basic.google-dataset.json new file mode 100644 index 00000000000..8501b6ded65 --- /dev/null +++ b/osf_tests/metadata/expected_metadata_files/preprint_basic.google-dataset.json @@ -0,0 +1,27 @@ +{ + "@context": "https://schema.org", + "@type": "Dataset", + "creator": [ + { + "@type": "Person", + "familyName": "McNamington", + "givenName": "Person", + "name": "Person McNamington" + } + ], + "dateCreated": "2123-05-04", + "dateModified": "2123-05-04", + "description": "No description was included in this Dataset collected from the OSF", + "identifier": [ + "https://doi.org/11.pp/FK2osf.io/w4ibb_v1", + "http://localhost:5000/w4ibb" + ], + "keywords": [], + "license": [], + "name": "this is a preprint title!", + "publisher": { + "@type": "Organization", + "name": "Center For Open Science" + }, + "url": "http://localhost:5000/w4ibb" +} \ No newline at end of file diff --git a/osf_tests/metadata/expected_metadata_files/preprint_basic.turtle b/osf_tests/metadata/expected_metadata_files/preprint_basic.turtle index 4d3627b928f..51d2b055346 100644 --- a/osf_tests/metadata/expected_metadata_files/preprint_basic.turtle +++ b/osf_tests/metadata/expected_metadata_files/preprint_basic.turtle @@ -75,6 +75,8 @@ a dcterms:Agent, foaf:Person ; dcterms:identifier "http://localhost:5000/w1ibb" ; + foaf:givenName "Person" ; + foaf:familyName "McNamington" ; foaf:name "Person McNamington" . a skos:ConceptScheme ; diff --git a/osf_tests/metadata/expected_metadata_files/preprint_full.google-dataset.json b/osf_tests/metadata/expected_metadata_files/preprint_full.google-dataset.json new file mode 100644 index 00000000000..8501b6ded65 --- /dev/null +++ b/osf_tests/metadata/expected_metadata_files/preprint_full.google-dataset.json @@ -0,0 +1,27 @@ +{ + "@context": "https://schema.org", + "@type": "Dataset", + "creator": [ + { + "@type": "Person", + "familyName": "McNamington", + "givenName": "Person", + "name": "Person McNamington" + } + ], + "dateCreated": "2123-05-04", + "dateModified": "2123-05-04", + "description": "No description was included in this Dataset collected from the OSF", + "identifier": [ + "https://doi.org/11.pp/FK2osf.io/w4ibb_v1", + "http://localhost:5000/w4ibb" + ], + "keywords": [], + "license": [], + "name": "this is a preprint title!", + "publisher": { + "@type": "Organization", + "name": "Center For Open Science" + }, + "url": "http://localhost:5000/w4ibb" +} \ No newline at end of file diff --git a/osf_tests/metadata/expected_metadata_files/preprint_full.turtle b/osf_tests/metadata/expected_metadata_files/preprint_full.turtle index 448c04ab644..6b28e0dfa3e 100644 --- a/osf_tests/metadata/expected_metadata_files/preprint_full.turtle +++ b/osf_tests/metadata/expected_metadata_files/preprint_full.turtle @@ -97,6 +97,8 @@ a dcterms:Agent, foaf:Person ; dcterms:identifier "http://localhost:5000/w1ibb" ; + foaf:givenName "Person" ; + foaf:familyName "McNamington" ; foaf:name "Person McNamington" . a skos:ConceptScheme ; diff --git a/osf_tests/metadata/expected_metadata_files/project_basic.datacite.json b/osf_tests/metadata/expected_metadata_files/project_basic.datacite.json index d866a786a89..7ed1a4b8ce5 100644 --- a/osf_tests/metadata/expected_metadata_files/project_basic.datacite.json +++ b/osf_tests/metadata/expected_metadata_files/project_basic.datacite.json @@ -42,7 +42,7 @@ ], "descriptions": [ { - "description": "this is a project description!", + "description": "this is a project description! it describes the project and is more than fifty characters long", "descriptionType": "Abstract" } ], diff --git a/osf_tests/metadata/expected_metadata_files/project_basic.datacite.xml b/osf_tests/metadata/expected_metadata_files/project_basic.datacite.xml index d395415a708..6dcaff2699b 100644 --- a/osf_tests/metadata/expected_metadata_files/project_basic.datacite.xml +++ b/osf_tests/metadata/expected_metadata_files/project_basic.datacite.xml @@ -32,7 +32,7 @@ No license - this is a project description! + this is a project description! it describes the project and is more than fifty characters long diff --git a/osf_tests/metadata/expected_metadata_files/project_basic.google-dataset.json b/osf_tests/metadata/expected_metadata_files/project_basic.google-dataset.json new file mode 100644 index 00000000000..4c633515f0d --- /dev/null +++ b/osf_tests/metadata/expected_metadata_files/project_basic.google-dataset.json @@ -0,0 +1,35 @@ +{ + "@context": "https://schema.org", + "@type": "Dataset", + "creator": [ + { + "@type": "Person", + "familyName": "McNamington", + "givenName": "Person", + "name": "Person McNamington" + } + ], + "dateCreated": "2123-05-04", + "dateModified": "2123-05-04", + "description": "this is a project description! it describes the project and is more than fifty characters long", + "identifier": [ + "http://localhost:5000/w2ibb", + "https://doi.org/10.70102/FK2osf.io/w2ibb" + ], + "keywords": [], + "license": [ + { + "@type": "CreativeWork", + "name": [ + "No license" + ], + "url": [] + } + ], + "name": "this is a project title!", + "publisher": { + "@type": "Organization", + "name": "Center For Open Science" + }, + "url": "http://localhost:5000/w2ibb" +} \ No newline at end of file diff --git a/osf_tests/metadata/expected_metadata_files/project_basic.turtle b/osf_tests/metadata/expected_metadata_files/project_basic.turtle index c5208ec295e..0bd7a45d606 100644 --- a/osf_tests/metadata/expected_metadata_files/project_basic.turtle +++ b/osf_tests/metadata/expected_metadata_files/project_basic.turtle @@ -12,7 +12,7 @@ dcterms:created "2123-05-04" ; dcterms:creator ; dcterms:dateCopyrighted "2252" ; - dcterms:description "this is a project description!" ; + dcterms:description "this is a project description! it describes the project and is more than fifty characters long" ; dcterms:hasVersion ; dcterms:identifier "http://localhost:5000/w2ibb", "https://doi.org/10.70102/FK2osf.io/w2ibb" ; @@ -94,6 +94,8 @@ a dcterms:Agent, foaf:Person ; dcterms:identifier "http://localhost:5000/w1ibb" ; + foaf:givenName "Person" ; + foaf:familyName "McNamington" ; foaf:name "Person McNamington" . a dcterms:Agent, diff --git a/osf_tests/metadata/expected_metadata_files/project_full.datacite.json b/osf_tests/metadata/expected_metadata_files/project_full.datacite.json index f4a43d07bd6..312e74b2388 100644 --- a/osf_tests/metadata/expected_metadata_files/project_full.datacite.json +++ b/osf_tests/metadata/expected_metadata_files/project_full.datacite.json @@ -42,7 +42,7 @@ ], "descriptions": [ { - "description": "this is a project description!", + "description": "this is a project description! it describes the project and is more than fifty characters long", "descriptionType": "Abstract", "lang": "en" } diff --git a/osf_tests/metadata/expected_metadata_files/project_full.datacite.xml b/osf_tests/metadata/expected_metadata_files/project_full.datacite.xml index f707bb2e077..524fbc33dd4 100644 --- a/osf_tests/metadata/expected_metadata_files/project_full.datacite.xml +++ b/osf_tests/metadata/expected_metadata_files/project_full.datacite.xml @@ -33,7 +33,7 @@ CC-By Attribution-NonCommercial-NoDerivatives 4.0 International - this is a project description! + this is a project description! it describes the project and is more than fifty characters long diff --git a/osf_tests/metadata/expected_metadata_files/project_full.google-dataset.json b/osf_tests/metadata/expected_metadata_files/project_full.google-dataset.json new file mode 100644 index 00000000000..70d64c0039d --- /dev/null +++ b/osf_tests/metadata/expected_metadata_files/project_full.google-dataset.json @@ -0,0 +1,37 @@ +{ + "@context": "https://schema.org", + "@type": "Dataset", + "creator": [ + { + "@type": "Person", + "familyName": "McNamington", + "givenName": "Person", + "name": "Person McNamington" + } + ], + "dateCreated": "2123-05-04", + "dateModified": "2123-05-04", + "description": "this is a project description! it describes the project and is more than fifty characters long", + "identifier": [ + "http://localhost:5000/w2ibb", + "https://doi.org/10.70102/FK2osf.io/w2ibb" + ], + "keywords": [], + "license": [ + { + "@type": "CreativeWork", + "name": [ + "CC-By Attribution-NonCommercial-NoDerivatives 4.0 International" + ], + "url": [ + "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode" + ] + } + ], + "name": "this is a project title!", + "publisher": { + "@type": "Organization", + "name": "Center For Open Science" + }, + "url": "http://localhost:5000/w2ibb" +} \ No newline at end of file diff --git a/osf_tests/metadata/expected_metadata_files/project_full.turtle b/osf_tests/metadata/expected_metadata_files/project_full.turtle index 6856faa651f..0085e6164e3 100644 --- a/osf_tests/metadata/expected_metadata_files/project_full.turtle +++ b/osf_tests/metadata/expected_metadata_files/project_full.turtle @@ -12,7 +12,7 @@ dcterms:created "2123-05-04" ; dcterms:creator ; dcterms:dateCopyrighted "2250-2254" ; - dcterms:description "this is a project description!"@en ; + dcterms:description "this is a project description! it describes the project and is more than fifty characters long"@en ; dcterms:hasVersion ; dcterms:identifier "http://localhost:5000/w2ibb", "https://doi.org/10.70102/FK2osf.io/w2ibb" ; @@ -123,6 +123,8 @@ a dcterms:Agent, foaf:Person ; dcterms:identifier "http://localhost:5000/w1ibb" ; + foaf:givenName "Person" ; + foaf:familyName "McNamington" ; foaf:name "Person McNamington" . a dcterms:Agent, diff --git a/osf_tests/metadata/expected_metadata_files/registration_basic.datacite.json b/osf_tests/metadata/expected_metadata_files/registration_basic.datacite.json index 17dc0757fb1..181af577b7d 100644 --- a/osf_tests/metadata/expected_metadata_files/registration_basic.datacite.json +++ b/osf_tests/metadata/expected_metadata_files/registration_basic.datacite.json @@ -42,7 +42,7 @@ ], "descriptions": [ { - "description": "this is a project description!", + "description": "this is a project description! it describes the project and is more than fifty characters long", "descriptionType": "Abstract" } ], diff --git a/osf_tests/metadata/expected_metadata_files/registration_basic.datacite.xml b/osf_tests/metadata/expected_metadata_files/registration_basic.datacite.xml index c96cb42c4ef..5f900f46d27 100644 --- a/osf_tests/metadata/expected_metadata_files/registration_basic.datacite.xml +++ b/osf_tests/metadata/expected_metadata_files/registration_basic.datacite.xml @@ -32,7 +32,7 @@ No license - this is a project description! + this is a project description! it describes the project and is more than fifty characters long diff --git a/osf_tests/metadata/expected_metadata_files/registration_basic.google-dataset.json b/osf_tests/metadata/expected_metadata_files/registration_basic.google-dataset.json new file mode 100644 index 00000000000..05792509901 --- /dev/null +++ b/osf_tests/metadata/expected_metadata_files/registration_basic.google-dataset.json @@ -0,0 +1,41 @@ +{ + "@context": "https://schema.org", + "@type": "Dataset", + "creator": [ + { + "@type": "Person", + "familyName": "McNamington", + "givenName": "Person", + "name": "Person McNamington" + } + ], + "dateCreated": "2123-05-04", + "dateModified": "2123-05-04", + "description": "this is a project description! it describes the project and is more than fifty characters long", + "distribution": [ + { + "@type": "DataDownload", + "contentUrl": "http://localhost:7777/v1/resources/w5ibb/providers/osfstorage/?zip=", + "encodingFormat": "URL" + } + ], + "identifier": [ + "http://localhost:5000/w5ibb" + ], + "keywords": [], + "license": [ + { + "@type": "CreativeWork", + "name": [ + "No license" + ], + "url": [] + } + ], + "name": "this is a project title!", + "publisher": { + "@type": "Organization", + "name": "Center For Open Science" + }, + "url": "http://localhost:5000/w5ibb" +} \ No newline at end of file diff --git a/osf_tests/metadata/expected_metadata_files/registration_basic.turtle b/osf_tests/metadata/expected_metadata_files/registration_basic.turtle index 562ded6c88a..49999661b58 100644 --- a/osf_tests/metadata/expected_metadata_files/registration_basic.turtle +++ b/osf_tests/metadata/expected_metadata_files/registration_basic.turtle @@ -13,7 +13,7 @@ dcterms:created "2123-05-04" ; dcterms:creator ; dcterms:dateCopyrighted "2252" ; - dcterms:description "this is a project description!" ; + dcterms:description "this is a project description! it describes the project and is more than fifty characters long" ; dcterms:identifier "http://localhost:5000/w5ibb" ; dcterms:isVersionOf ; dcterms:modified "2123-05-04" ; @@ -77,6 +77,8 @@ a dcterms:Agent, foaf:Person ; dcterms:identifier "http://localhost:5000/w1ibb" ; + foaf:givenName "Person" ; + foaf:familyName "McNamington" ; foaf:name "Person McNamington" . a dcterms:Agent, diff --git a/osf_tests/metadata/expected_metadata_files/registration_full.datacite.json b/osf_tests/metadata/expected_metadata_files/registration_full.datacite.json index 25934481a42..6d8118f81bb 100644 --- a/osf_tests/metadata/expected_metadata_files/registration_full.datacite.json +++ b/osf_tests/metadata/expected_metadata_files/registration_full.datacite.json @@ -42,7 +42,7 @@ ], "descriptions": [ { - "description": "this is a project description!", + "description": "this is a project description! it describes the project and is more than fifty characters long", "descriptionType": "Abstract" } ], diff --git a/osf_tests/metadata/expected_metadata_files/registration_full.datacite.xml b/osf_tests/metadata/expected_metadata_files/registration_full.datacite.xml index ec32ad31151..ce47bc15d6a 100644 --- a/osf_tests/metadata/expected_metadata_files/registration_full.datacite.xml +++ b/osf_tests/metadata/expected_metadata_files/registration_full.datacite.xml @@ -32,7 +32,7 @@ CC-By Attribution-NonCommercial-NoDerivatives 4.0 International - this is a project description! + this is a project description! it describes the project and is more than fifty characters long diff --git a/osf_tests/metadata/expected_metadata_files/registration_full.google-dataset.json b/osf_tests/metadata/expected_metadata_files/registration_full.google-dataset.json new file mode 100644 index 00000000000..685e6d46c84 --- /dev/null +++ b/osf_tests/metadata/expected_metadata_files/registration_full.google-dataset.json @@ -0,0 +1,43 @@ +{ + "@context": "https://schema.org", + "@type": "Dataset", + "creator": [ + { + "@type": "Person", + "familyName": "McNamington", + "givenName": "Person", + "name": "Person McNamington" + } + ], + "dateCreated": "2123-05-04", + "dateModified": "2123-05-04", + "description": "this is a project description! it describes the project and is more than fifty characters long", + "distribution": [ + { + "@type": "DataDownload", + "contentUrl": "http://localhost:7777/v1/resources/w5ibb/providers/osfstorage/?zip=", + "encodingFormat": "URL" + } + ], + "identifier": [ + "http://localhost:5000/w5ibb" + ], + "keywords": [], + "license": [ + { + "@type": "CreativeWork", + "name": [ + "CC-By Attribution-NonCommercial-NoDerivatives 4.0 International" + ], + "url": [ + "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode" + ] + } + ], + "name": "this is a project title!", + "publisher": { + "@type": "Organization", + "name": "Center For Open Science" + }, + "url": "http://localhost:5000/w5ibb" +} \ No newline at end of file diff --git a/osf_tests/metadata/expected_metadata_files/registration_full.turtle b/osf_tests/metadata/expected_metadata_files/registration_full.turtle index 03d711b8f79..ab75ae5888c 100644 --- a/osf_tests/metadata/expected_metadata_files/registration_full.turtle +++ b/osf_tests/metadata/expected_metadata_files/registration_full.turtle @@ -13,7 +13,7 @@ dcterms:created "2123-05-04" ; dcterms:creator ; dcterms:dateCopyrighted "2250-2254" ; - dcterms:description "this is a project description!" ; + dcterms:description "this is a project description! it describes the project and is more than fifty characters long" ; dcterms:identifier "http://localhost:5000/w5ibb" ; dcterms:isVersionOf ; dcterms:modified "2123-05-04" ; @@ -105,6 +105,8 @@ a dcterms:Agent, foaf:Person ; dcterms:identifier "http://localhost:5000/w1ibb" ; + foaf:givenName "Person" ; + foaf:familyName "McNamington" ; foaf:name "Person McNamington" . a dcterms:Agent, diff --git a/osf_tests/metadata/expected_metadata_files/user_basic.turtle b/osf_tests/metadata/expected_metadata_files/user_basic.turtle index 8ebcc616171..39e69297a11 100644 --- a/osf_tests/metadata/expected_metadata_files/user_basic.turtle +++ b/osf_tests/metadata/expected_metadata_files/user_basic.turtle @@ -6,4 +6,6 @@ foaf:Person ; dcat:accessService ; dcterms:identifier "http://localhost:5000/w1ibb" ; + foaf:givenName "Person" ; + foaf:familyName "McNamington" ; foaf:name "Person McNamington" . diff --git a/osf_tests/metadata/expected_metadata_files/user_full.turtle b/osf_tests/metadata/expected_metadata_files/user_full.turtle index 8ebcc616171..39e69297a11 100644 --- a/osf_tests/metadata/expected_metadata_files/user_full.turtle +++ b/osf_tests/metadata/expected_metadata_files/user_full.turtle @@ -6,4 +6,6 @@ foaf:Person ; dcat:accessService ; dcterms:identifier "http://localhost:5000/w1ibb" ; + foaf:givenName "Person" ; + foaf:familyName "McNamington" ; foaf:name "Person McNamington" . diff --git a/osf_tests/metadata/test_google_datasets.py b/osf_tests/metadata/test_google_datasets.py deleted file mode 100644 index e479b8160a8..00000000000 --- a/osf_tests/metadata/test_google_datasets.py +++ /dev/null @@ -1,25 +0,0 @@ -from tests.base import OsfTestCase -from osf_tests import factories -from osf.metadata.tools import pls_gather_metadata_as_dict -from osf.metadata.serializers import GoogleDatasetJsonLdSerializer - - -class TestGoogleDatasetJsonLdSerializer(OsfTestCase): - - def setUp(self): - super().setUp() - self.project_description_short = factories.ProjectFactory(description='Under 50') - self.project_description_null = factories.ProjectFactory(description='') - self.project_description_50_chars = factories.ProjectFactory(description='N' * 50) - - def test_description_short(self): - result_metadata = pls_gather_metadata_as_dict(self.project_description_short, 'google-dataset-json-ld') - assert result_metadata['description'] == GoogleDatasetJsonLdSerializer.DEFAULT_DESCRIPTION - - def test_description_null(self): - result_metadata = pls_gather_metadata_as_dict(self.project_description_null, 'google-dataset-json-ld') - assert result_metadata['description'] == GoogleDatasetJsonLdSerializer.DEFAULT_DESCRIPTION - - def test_description_50(self): - result_metadata = pls_gather_metadata_as_dict(self.project_description_50_chars, 'google-dataset-json-ld') - assert result_metadata['description'] == self.project_description_50_chars.description diff --git a/osf_tests/metadata/test_osf_gathering.py b/osf_tests/metadata/test_osf_gathering.py index 33be346e2df..b20f7477070 100644 --- a/osf_tests/metadata/test_osf_gathering.py +++ b/osf_tests/metadata/test_osf_gathering.py @@ -665,11 +665,15 @@ def test_gather_user_basics(self): assert_triples(osf_gathering.gather_user_basics(self.userfocus__admin), { (self.userfocus__admin.iri, RDF.type, FOAF.Person), (self.userfocus__admin.iri, FOAF.name, Literal(self.user__admin.fullname)), + (self.userfocus__admin.iri, FOAF.givenName, Literal(self.user__admin.given_name)), + (self.userfocus__admin.iri, FOAF.familyName, Literal(self.user__admin.family_name)), }) # focus: readwrite user assert_triples(osf_gathering.gather_user_basics(self.userfocus__readwrite), { (self.userfocus__readwrite.iri, RDF.type, FOAF.Person), (self.userfocus__readwrite.iri, FOAF.name, Literal(self.user__readwrite.fullname)), + (self.userfocus__readwrite.iri, FOAF.givenName, Literal(self.user__readwrite.given_name)), + (self.userfocus__readwrite.iri, FOAF.familyName, Literal(self.user__readwrite.family_name)), (self.userfocus__readwrite.iri, DCTERMS.identifier, Literal('https://orcid.org/1234-4321-5678-8765')), (self.userfocus__readwrite.iri, OWL.sameAs, URIRef('https://orcid.org/1234-4321-5678-8765')), }) @@ -677,6 +681,8 @@ def test_gather_user_basics(self): assert_triples(osf_gathering.gather_user_basics(self.userfocus__readonly), { (self.userfocus__readonly.iri, RDF.type, FOAF.Person), (self.userfocus__readonly.iri, FOAF.name, Literal(self.user__readonly.fullname)), + (self.userfocus__readonly.iri, FOAF.givenName, Literal(self.user__readonly.given_name)), + (self.userfocus__readonly.iri, FOAF.familyName, Literal(self.user__readonly.family_name)), # orcid not verified, should be excluded (self.userfocus__readonly.iri, DCTERMS.identifier, Literal('http://mysite.example')), (self.userfocus__readonly.iri, DCTERMS.identifier, Literal('http://myothersite.example/foo')), diff --git a/osf_tests/metadata/test_serialized_metadata.py b/osf_tests/metadata/test_serialized_metadata.py index bcbb18a52ae..e8b6242ac5e 100644 --- a/osf_tests/metadata/test_serialized_metadata.py +++ b/osf_tests/metadata/test_serialized_metadata.py @@ -30,6 +30,7 @@ 'turtle': 'project_basic.turtle', 'datacite-xml': 'project_basic.datacite.xml', 'datacite-json': 'project_basic.datacite.json', + 'google-dataset-json-ld': 'project_basic.google-dataset.json', }, }, OSF.Preprint: { @@ -37,6 +38,7 @@ 'turtle': 'preprint_basic.turtle', 'datacite-xml': 'preprint_basic.datacite.xml', 'datacite-json': 'preprint_basic.datacite.json', + 'google-dataset-json-ld': 'preprint_basic.google-dataset.json', }, }, OSF.Registration: { @@ -44,6 +46,7 @@ 'turtle': 'registration_basic.turtle', 'datacite-xml': 'registration_basic.datacite.xml', 'datacite-json': 'registration_basic.datacite.json', + 'google-dataset-json-ld': 'registration_basic.google-dataset.json', }, }, OSF.File: { @@ -51,6 +54,7 @@ 'turtle': 'file_basic.turtle', 'datacite-xml': 'file_basic.datacite.xml', 'datacite-json': 'file_basic.datacite.json', + 'google-dataset-json-ld': 'file_basic.google-dataset.json', }, }, DCTERMS.Agent: { @@ -66,6 +70,7 @@ 'turtle': 'project_full.turtle', 'datacite-xml': 'project_full.datacite.xml', 'datacite-json': 'project_full.datacite.json', + 'google-dataset-json-ld': 'project_full.google-dataset.json', }, OsfmapPartition.SUPPLEMENT: { 'turtle': 'project_supplement.turtle', @@ -79,6 +84,7 @@ 'turtle': 'preprint_full.turtle', 'datacite-xml': 'preprint_full.datacite.xml', 'datacite-json': 'preprint_full.datacite.json', + 'google-dataset-json-ld': 'preprint_full.google-dataset.json', }, OsfmapPartition.SUPPLEMENT: { 'turtle': 'preprint_supplement.turtle', @@ -92,6 +98,7 @@ 'turtle': 'registration_full.turtle', 'datacite-xml': 'registration_full.datacite.xml', 'datacite-json': 'registration_full.datacite.json', + 'google-dataset-json-ld': 'registration_full.google-dataset.json', }, OsfmapPartition.SUPPLEMENT: { 'turtle': 'registration_supplement.turtle', @@ -105,6 +112,7 @@ 'turtle': 'file_full.turtle', 'datacite-xml': 'file_full.datacite.xml', 'datacite-json': 'file_full.datacite.json', + 'google-dataset-json-ld': 'file_full.google-dataset.json', }, OsfmapPartition.SUPPLEMENT: { 'turtle': 'file_supplement.turtle', @@ -130,6 +138,7 @@ 'turtle': 'text/turtle; charset=utf-8', 'datacite-xml': 'application/xml', 'datacite-json': 'application/json', + 'google-dataset-json-ld': 'application/ld+json', } @@ -176,6 +185,7 @@ def setUp(self): mock.patch('osf.models.base.generate_guid', new=osfguid_sequence), mock.patch('osf.models.base.Guid.objects.get_or_create', new=osfguid_sequence.get_or_create), mock.patch('django.utils.timezone.now', new=forever_now), + mock.patch('osf.models.mixins.timezone.now', new=forever_now), mock.patch('osf.models.metaschema.RegistrationSchema.absolute_api_v2_url', new='http://fake.example/schema/for/test'), mock.patch('osf.models.node.Node.get_verified_links', return_value=[ {'target_url': 'https://foo.bar', 'resource_type': 'Other'} @@ -190,7 +200,7 @@ def setUp(self): is_public=True, creator=self.user, title='this is a project title!', - description='this is a project description!', + description='this is a project description! it describes the project and is more than fifty characters long', node_license=factories.NodeLicenseRecordFactory( node_license=NodeLicense.objects.get( name='No license', diff --git a/osf_tests/test_gv_addon_logs.py b/osf_tests/test_gv_addon_logs.py index 8719151aac9..6726088a031 100644 --- a/osf_tests/test_gv_addon_logs.py +++ b/osf_tests/test_gv_addon_logs.py @@ -9,6 +9,10 @@ @pytest.mark.django_db class TestGVAddonLogs: + @pytest.fixture(autouse=True) + def _patch_update_search(self): + return patch('osf.models.AbstractNode.update_search') + @pytest.fixture() def user(self): return UserFactory() diff --git a/website/templates/project/modal_generate_private_link.mako b/website/templates/project/modal_generate_private_link.mako index ce60737e362..30d2d2c88e2 100644 --- a/website/templates/project/modal_generate_private_link.mako +++ b/website/templates/project/modal_generate_private_link.mako @@ -2,7 +2,7 @@