diff --git a/CHANGELOG b/CHANGELOG index 6d71b42cb98..d3560237310 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,11 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. +25.16.0 (2025-09-17) +==================== + +- Misc. fixes for Angular migration + 25.15.2 (2025-09-10) ==================== 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 a7416f66d0d..41ac9583fe8 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 @@ -48,6 +47,7 @@ AddSystemTagForm ) from admin.base.views import GuidView +from api.users.services import send_password_reset_email from website.settings import DOMAIN, OSF_SUPPORT_EMAIL from django.urls import reverse_lazy @@ -532,17 +532,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/admin_tests/users/test_views.py b/admin_tests/users/test_views.py index 9e30a7f689d..a8ccb5618a8 100644 --- a/admin_tests/users/test_views.py +++ b/admin_tests/users/test_views.py @@ -26,7 +26,7 @@ from admin.users import views from admin.users.forms import UserSearchForm, MergeUserForm from osf.models.admin_log_entry import AdminLogEntry -from tests.utils import assert_notification +from tests.utils import assert_notification, capture_notifications from osf.models.notification_type import NotificationType pytestmark = pytest.mark.django_db @@ -101,7 +101,11 @@ def test_correct_view_permissions(self): request.POST = {'emails': ', '.join(user.emails.all().values_list('address', flat=True))} request.user = user - response = views.ResetPasswordView.as_view()(request, guid=guid) + with capture_notifications() as notifications: + response = views.ResetPasswordView.as_view()(request, guid=guid) + + assert len(notifications['emits']) == 1 + assert notifications['emits'][0]['type'] == NotificationType.Type.USER_FORGOT_PASSWORD self.assertEqual(response.status_code, 302) 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 0aaf42fd2fb..0d4361d666c 100644 --- a/api/nodes/serializers.py +++ b/api/nodes/serializers.py @@ -1698,12 +1698,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 2cb42c8cdb0..42ad6ffaad2 100644 --- a/api/preprints/serializers.py +++ b/api/preprints/serializers.py @@ -104,6 +104,7 @@ class PreprintSerializer(TaxonomizableSerializerMixin, MetricsSerializerMixin, J 'reviews_state', 'node_is_public', 'tags', + 'description', ]) available_metrics = frozenset([ 'downloads', @@ -155,6 +156,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( @@ -191,6 +196,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( @@ -501,11 +507,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 @@ -527,11 +552,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/providers/serializers.py b/api/providers/serializers.py index 2e5a59903a6..6e95e263d2a 100644 --- a/api/providers/serializers.py +++ b/api/providers/serializers.py @@ -364,7 +364,6 @@ def create(self, validated_data): context['provider__id'] = provider._id context['is_reviews_moderator_notification'] = True context['referrer_fullname'] = user.fullname - context['user_fullname'] = user.fullname context['is_reviews_moderator_notification'] = True context['is_admin'] = perm_group == ADMIN diff --git a/api/users/serializers.py b/api/users/serializers.py index e48153feda9..fddffb4a2b7 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..9237f0b1d9f --- /dev/null +++ b/api/users/services.py @@ -0,0 +1,30 @@ +from furl import furl +from django.utils import timezone + +from framework.auth.core import generate_verification_key +from osf.models import NotificationType +from website import settings + + +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 + notification_type = NotificationType.Type.USER_FORGOT_PASSWORD_INSTITUTION if institutional \ + else NotificationType.Type.USER_FORGOT_PASSWORD + + notification_type.instance.emit( + destination_address=email, + event_context={ + '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 e9768ebf5e2..e982ca1e672 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -822,6 +822,7 @@ 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.') @@ -865,12 +866,12 @@ def get(self, request, *args, **kwargs): status=status.HTTP_200_OK, data={ 'message': status_message, + '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/mailhog/provider/test_preprints.py b/api_tests/mailhog/provider/test_preprints.py index 9e51b2c710e..db514c87c34 100644 --- a/api_tests/mailhog/provider/test_preprints.py +++ b/api_tests/mailhog/provider/test_preprints.py @@ -2,7 +2,7 @@ from osf import features from framework.auth.core import Auth -from osf.models import NotificationType +from osf.models import NotificationType, Notification from osf_tests.factories import ( ProjectFactory, AuthUserFactory, @@ -12,6 +12,7 @@ from osf.utils.permissions import WRITE from tests.base import OsfTestCase from tests.utils import get_mailhog_messages, delete_mailhog_messages, capture_notifications, assert_emails +from notifications.tasks import send_users_instant_digest_email class TestPreprintConfirmationEmails(OsfTestCase): @@ -28,21 +29,31 @@ def setUp(self): @override_switch(features.ENABLE_MAILHOG, active=True) def test_creator_gets_email(self): delete_mailhog_messages() + with capture_notifications(passthrough=True) as notifications: self.preprint.set_published(True, auth=Auth(self.user), save=True) assert len(notifications['emits']) == 1 assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION - massages = get_mailhog_messages() - assert massages['count'] == len(notifications['emails']) - assert_emails(massages, notifications) + messages = get_mailhog_messages() + assert not messages['items'] + assert Notification.objects.all() + with capture_notifications(passthrough=True) as notifications: + send_users_instant_digest_email.delay() + + messages = get_mailhog_messages() + assert messages['count'] == len(notifications['emits']) delete_mailhog_messages() with capture_notifications(passthrough=True) as notifications: self.preprint_branded.set_published(True, auth=Auth(self.user), save=True) assert len(notifications['emits']) == 1 assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION + messages = get_mailhog_messages() + assert not messages['items'] + with capture_notifications(passthrough=True) as notifications: + send_users_instant_digest_email.delay() massages = get_mailhog_messages() - assert massages['count'] == len(notifications['emails']) + assert massages['count'] == len(notifications['emits']) assert_emails(massages, notifications) delete_mailhog_messages() diff --git a/api_tests/mailhog/provider/test_reviewable.py b/api_tests/mailhog/provider/test_reviewable.py index c4a01752542..c493c6a3b22 100644 --- a/api_tests/mailhog/provider/test_reviewable.py +++ b/api_tests/mailhog/provider/test_reviewable.py @@ -6,7 +6,6 @@ from osf_tests.factories import PreprintFactory, AuthUserFactory from tests.utils import get_mailhog_messages, delete_mailhog_messages, capture_notifications, assert_emails - @pytest.mark.django_db class TestReviewable: @@ -18,16 +17,11 @@ def test_reject_resubmission_sends_emails(self): is_published=False ) assert preprint.machine_state == DefaultStates.INITIAL.value - delete_mailhog_messages() with capture_notifications(passthrough=True) as notifications: preprint.run_submit(user) assert len(notifications['emits']) == 1 assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION assert preprint.machine_state == DefaultStates.PENDING.value - - massages = get_mailhog_messages() - assert massages['count'] == len(notifications['emails']) - assert_emails(massages, notifications) delete_mailhog_messages() assert not user.notification_subscriptions.exists() @@ -49,8 +43,9 @@ def test_reject_resubmission_sends_emails(self): assert len(notifications['emits']) == 1 assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_RESUBMISSION_CONFIRMATION assert preprint.machine_state == DefaultStates.PENDING.value - massages = get_mailhog_messages() - assert massages['count'] == len(notifications['emails']) - assert_emails(massages, notifications) + + messages = get_mailhog_messages() + assert messages['count'] == len(notifications['emits']) + assert_emails(messages, notifications) delete_mailhog_messages() diff --git a/api_tests/mailhog/provider/test_schema_responses.py b/api_tests/mailhog/provider/test_schema_responses.py index 8a36fe130a2..f8fec0b42bf 100644 --- a/api_tests/mailhog/provider/test_schema_responses.py +++ b/api_tests/mailhog/provider/test_schema_responses.py @@ -244,9 +244,9 @@ def test_moderator_accept_notification( with capture_notifications(passthrough=True) as notifications: revised_response.accept(user=moderator) assert len(notifications['emits']) == 3 - massages = get_mailhog_messages() - assert massages['count'] == len(notifications['emails']) - assert_emails(massages, notifications) + messages = get_mailhog_messages() + assert messages['count'] == len(notifications['emails']) + assert_emails(messages, notifications) delete_mailhog_messages() @@ -259,8 +259,8 @@ def test_moderator_reject_notification( with capture_notifications(passthrough=True) as notifications: revised_response.reject(user=moderator) assert len(notifications['emits']) == 3 - massages = get_mailhog_messages() - assert massages['count'] == len(notifications['emails']) - assert_emails(massages, notifications) + messages = get_mailhog_messages() + assert messages['count'] == len(notifications['emails']) + assert_emails(messages, notifications) delete_mailhog_messages() diff --git a/api_tests/mailhog/provider/test_submissions.py b/api_tests/mailhog/provider/test_submissions.py index a9142f98f80..caa0abe71b0 100644 --- a/api_tests/mailhog/provider/test_submissions.py +++ b/api_tests/mailhog/provider/test_submissions.py @@ -1,5 +1,7 @@ import pytest from waffle.testutils import override_switch + +from notifications.tasks import send_users_instant_digest_email from osf import features from api.base.settings.defaults import API_BASE @@ -19,7 +21,7 @@ from osf.models import NotificationType from osf.migrations import update_provider_auth_groups -from tests.utils import capture_notifications, get_mailhog_messages, delete_mailhog_messages, assert_emails +from tests.utils import capture_notifications, get_mailhog_messages, delete_mailhog_messages @pytest.mark.django_db @@ -82,9 +84,9 @@ def test_get_registration_actions(self, app, registration_actions_url, registrat assert len(notifications['emits']) == 2 assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS assert notifications['emits'][1]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_WITHDRAW_REQUESTS - massages = get_mailhog_messages() - assert massages['count'] == len(notifications['emails']) - assert_emails(massages, notifications) + messages = get_mailhog_messages() + assert messages['count'] == 1 + assert messages['items'][0]['Content']['Headers']['To'][0] == registration.creator.username delete_mailhog_messages() @@ -116,9 +118,10 @@ def test_get_provider_actions(self, app, provider_actions_url, registration, mod assert len(notifications['emits']) == 2 assert notifications['emits'][0]['type'] == NotificationType.Type.PROVIDER_REVIEWS_SUBMISSION_CONFIRMATION assert notifications['emits'][1]['type'] == NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS - massages = get_mailhog_messages() - assert massages['count'] == len(notifications['emails']) - assert_emails(massages, notifications) + send_users_instant_digest_email.delay() + messages = get_mailhog_messages() + assert messages['count'] == 1 + assert messages['items'][0]['Content']['Headers']['To'][0] == registration.creator.username delete_mailhog_messages() 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/notifications/test_notification_digest.py b/api_tests/notifications/test_notification_digest.py index f5b194c8c48..be1f31ce3d5 100644 --- a/api_tests/notifications/test_notification_digest.py +++ b/api_tests/notifications/test_notification_digest.py @@ -184,6 +184,6 @@ def test_send_moderators_digest_email_end_to_end(fake): sent=None, event_context={'provider_id': provider.id}, ) - send_moderators_digest_email() + send_moderators_digest_email.delay() email_task = EmailTask.objects.filter(user_id=user.id).first() assert email_task.status == 'SUCCESS' 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_detail.py b/api_tests/users/views/test_user_settings_detail.py index 6ef4135fc99..3df6c2e5e5c 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/framework/auth/views.py b/framework/auth/views.py index 56146e481e0..4897430b420 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -847,7 +847,7 @@ def send_confirm_email(user, email, renew=False, external_id_provider=None, exte # Merge account confirmation merge_account_data = { 'merge_target_fullname': merge_target.fullname or merge_target.username, - 'user_username': user.fullname, + 'user_username': user.username, 'email': merge_target.email, } notification_type = NotificationType.Type.USER_CONFIRM_MERGE @@ -862,8 +862,7 @@ def send_confirm_email(user, email, renew=False, external_id_provider=None, exte notification_type = NotificationType.Type.USER_INITIAL_CONFIRM_EMAIL notification_type.instance.emit( - user=user, - subscribed_object=user, + destination_address=merge_target.address or user.username, event_context={ 'user_fullname': user.fullname, 'confirmation_url': confirmation_url, @@ -873,6 +872,7 @@ def send_confirm_email(user, email, renew=False, external_id_provider=None, exte 'osf_support_email': settings.OSF_SUPPORT_EMAIL, **merge_account_data, }, + save=False ) def send_confirm_email_async(user, email, renew=False, external_id_provider=None, external_id=None, destination=None): diff --git a/notifications/tasks.py b/notifications/tasks.py index f4470a884a4..dbe3ecb2c93 100644 --- a/notifications/tasks.py +++ b/notifications/tasks.py @@ -20,7 +20,7 @@ logger = get_task_logger(__name__) @celery_app.task(bind=True) -def send_user_email_task(self, user_id, notification_ids, message_freq): +def send_user_email_task(self, user_id, notification_ids, **kwargs): try: user = OSFUser.objects.get( guids___id=user_id, @@ -89,7 +89,7 @@ def send_user_email_task(self, user_id, notification_ids, message_freq): raise self.retry(exc=e) @celery_app.task(bind=True) -def send_moderator_email_task(self, user_id, provider_id, notification_ids, message_freq): +def send_moderator_email_task(self, user_id, provider_id, notification_ids, **kwargs): try: user = OSFUser.objects.get( guids___id=user_id, @@ -165,7 +165,6 @@ def send_moderator_email_task(self, user_id, provider_id, notification_ids, mess NotificationType.Type.DIGEST_REVIEWS_MODERATORS.instance.emit( user=user, event_context=event_context, - is_digest=True ) notifications_qs.update(sent=timezone.now()) @@ -196,7 +195,7 @@ def send_users_digest_email(dry_run=False): user_id = group['user_id'] notification_ids = [msg['notification_id'] for msg in group['info']] if not dry_run: - send_user_email_task.delay(user_id, notification_ids, freq) + send_user_email_task.delay(user_id, notification_ids) @celery_app.task(name='notifications.tasks.send_moderators_digest_email') def send_moderators_digest_email(dry_run=False): @@ -215,7 +214,7 @@ def send_moderators_digest_email(dry_run=False): provider_id = group['provider_id'] notification_ids = [msg['notification_id'] for msg in group['info']] if not dry_run: - send_moderator_email_task.delay(user_id, provider_id, notification_ids, freq) + send_moderator_email_task.delay(user_id, provider_id, notification_ids) def get_moderators_emails(message_freq: str): """Get all emails for reviews moderators that need to be sent, grouped by users AND providers. @@ -322,8 +321,8 @@ def remove_subscription_task(node_id): ).delete() -@celery_app.task(name='notifications.tasks.send_users_instant_digest_email') -def send_users_instant_digest_email(dry_run): +@celery_app.task(bind=True, name='notifications.tasks.send_users_instant_digest_email') +def send_users_instant_digest_email(self, dry_run=False, **kwargs): """Send pending "instant' digest emails. :return: """ @@ -332,10 +331,10 @@ def send_users_instant_digest_email(dry_run): user_id = group['user_id'] notification_ids = [msg['notification_id'] for msg in group['info']] if not dry_run: - send_user_email_task.delay(user_id, notification_ids, 'instantly') + send_user_email_task.delay(user_id, notification_ids) -@celery_app.task(name='notifications.tasks.send_moderators_instant_digest_email') -def send_moderators_instant_digest_email(dry_run=False): +@celery_app.task(bind=True, name='notifications.tasks.send_moderators_instant_digest_email') +def send_moderators_instant_digest_email(self, dry_run=False, **kwargs): """Send pending "instant' digest emails. :return: """ @@ -345,4 +344,4 @@ def send_moderators_instant_digest_email(dry_run=False): provider_id = group['provider_id'] notification_ids = [msg['notification_id'] for msg in group['info']] if not dry_run: - send_moderator_email_task.delay(user_id, provider_id, notification_ids, 'instantly') + send_moderator_email_task.delay(user_id, provider_id, notification_ids) 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/0003_aggregated_runsql_calls.py b/osf/migrations/0003_aggregated_runsql_calls.py index bf945b0f2dd..985bed65e86 100644 --- a/osf/migrations/0003_aggregated_runsql_calls.py +++ b/osf/migrations/0003_aggregated_runsql_calls.py @@ -11,6 +11,7 @@ class Migration(migrations.Migration): migrations.RunSQL( [ """ + CREATE UNIQUE INDEX one_quickfiles_per_user ON public.osf_abstractnode USING btree (creator_id, type, is_deleted) WHERE (((type)::text = 'osf.quickfilesnode'::text) AND (is_deleted = false)); CREATE INDEX osf_abstractnode_collection_pub_del_type_index ON public.osf_abstractnode USING btree (is_public, is_deleted, type) WHERE ((is_public = true) AND (is_deleted = false) AND ((type)::text = 'osf.collection'::text)); CREATE INDEX osf_abstractnode_date_modified_ef1e2ad8 ON public.osf_abstractnode USING btree (last_logged); CREATE INDEX osf_abstractnode_node_pub_del_type_index ON public.osf_abstractnode USING btree (is_public, is_deleted, type) WHERE ((is_public = true) AND (is_deleted = false) AND ((type)::text = 'osf.node'::text)); diff --git a/osf/migrations/0016_auto_20230828_1810.py b/osf/migrations/0016_auto_20230828_1810.py index 36f056c8ef1..50af929ea95 100644 --- a/osf/migrations/0016_auto_20230828_1810.py +++ b/osf/migrations/0016_auto_20230828_1810.py @@ -23,6 +23,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='abstractnode', name='type', - field=models.CharField(choices=[('osf.node', 'node'), ('osf.draftnode', 'draft node'), ('osf.registration', 'registration')], db_index=True, max_length=255), + field=models.CharField(choices=[('osf.node', 'node'), ('osf.draftnode', 'draft node'), ('osf.quickfilesnode', 'quick files node'), ('osf.registration', 'registration')], db_index=True, max_length=255), ), ] 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/migrations/0033_notification_system.py b/osf/migrations/0033_notification_system.py index 86b2f5e09e5..0b2a2dc5a88 100644 --- a/osf/migrations/0033_notification_system.py +++ b/osf/migrations/0033_notification_system.py @@ -17,6 +17,7 @@ class Migration(migrations.Migration): ] operations = [ + # This renames the legacy data so we have overwrite it later with migrate notifications management command migrations.RunSQL( """ DO $$ diff --git a/osf/migrations/0034_merge_20250917_1902.py b/osf/migrations/0034_merge_20250917_1902.py new file mode 100644 index 00000000000..fb3c7e92666 --- /dev/null +++ b/osf/migrations/0034_merge_20250917_1902.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.17 on 2025-09-17 19:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0033_merge_20250915_2100'), + ('osf', '0033_notification_system'), + ] + + 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 a8697a2f69c..f0c2cd1528b 100644 --- a/osf/models/mixins.py +++ b/osf/models/mixins.py @@ -141,7 +141,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: @@ -1085,7 +1085,8 @@ def add_to_group(self, user, group): user=user, content_type=ContentType.objects.get_for_model(self), object_id=self.id, - notification_type=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.instance + notification_type=NotificationType.Type.PROVIDER_NEW_PENDING_SUBMISSIONS.instance, + _is_digest=True ) def remove_from_group(self, user, group, unsubscribe=True): @@ -1096,21 +1097,13 @@ def remove_from_group(self, user, group, unsubscribe=True): if unsubscribe: # remove notification subscription for subscription in self.DEFAULT_SUBSCRIPTIONS: - self.remove_user_from_subscription(user, subscription) + NotificationSubscription.objects.filter( + notification_type=subscription.instance, + user=user, + ).delete() return _group.user_set.remove(user) - def remove_user_from_subscription(self, user, subscription): - notification_type = NotificationType.objects.get( - name=subscription, - ) - subscriptions = NotificationSubscription.objects.filter( - notification_type=notification_type, - user=user - ) - if subscriptions: - subscriptions.get().delete() - class TaxonomizableMixin(models.Model): class Meta: diff --git a/osf/models/notification_type.py b/osf/models/notification_type.py index 2b2a4105246..5df35767648 100644 --- a/osf/models/notification_type.py +++ b/osf/models/notification_type.py @@ -229,15 +229,6 @@ def emit( save=save, ) - def remove_user_from_subscription(self, user): - """ - """ - from osf.models.notification_subscription import NotificationSubscription - notification, _ = NotificationSubscription.objects.filter( - user=user, - notification_type=self, - ).delete() - def __str__(self) -> str: return self.name diff --git a/osf/models/preprint.py b/osf/models/preprint.py index afd8ac20110..69f694bb5ce 100644 --- a/osf/models/preprint.py +++ b/osf/models/preprint.py @@ -1049,6 +1049,7 @@ def _send_preprint_confirmation(self, auth): 'document_type': self.provider.preprint_word, 'notify_comment': not self.provider.reviews_comments_private }, + is_digest=True ) # FOLLOWING BEHAVIOR NOT SPECIFIC TO PREPRINTS 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 b472856e8a4..9b9f4eaaa98 100644 --- a/osf_tests/factories.py +++ b/osf_tests/factories.py @@ -1294,6 +1294,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 44c383be4c8..424042253f7 100644 --- a/osf_tests/metadata/test_osf_gathering.py +++ b/osf_tests/metadata/test_osf_gathering.py @@ -670,11 +670,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')), }) @@ -682,6 +686,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/osf_tests/test_registration_moderation_notifications.py b/osf_tests/test_registration_moderation_notifications.py index ea879368b44..43d9b23802e 100644 --- a/osf_tests/test_registration_moderation_notifications.py +++ b/osf_tests/test_registration_moderation_notifications.py @@ -3,7 +3,7 @@ from django.utils import timezone -from notifications.tasks import send_users_digest_email, send_moderators_digest_email +from notifications.tasks import send_users_digest_email from osf.management.commands.populate_notification_types import populate_notification_types from osf.migrations import update_provider_auth_groups from osf.models import Brand, NotificationSubscription, NotificationType @@ -196,4 +196,3 @@ def test_branded_provider_notification_renders(self, registration, admin, modera with capture_notifications(): notify_submit(registration, admin) - send_moderators_digest_email() diff --git a/package.json b/package.json index 882b7fd7096..72aa651c417 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "OSF", - "version": "25.15.2", + "version": "25.16.0", "description": "Facilitating Open Science", "repository": "https://github.com/CenterForOpenScience/osf.io", "author": "Center for Open Science", diff --git a/tasks/__init__.py b/tasks/__init__.py index 680afe87bc6..11dc4a292b9 100755 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -13,7 +13,6 @@ import invoke from invoke import Collection -from website import settings from .utils import pip_install, bin_prefix @@ -63,6 +62,8 @@ def decorator(f): @task def server(ctx, host=None, port=5000, debug=True, gitlogs=False): """Run the app server.""" + from website import settings + if os.environ.get('WERKZEUG_RUN_MAIN') == 'true' or not debug: if os.environ.get('WEB_REMOTE_DEBUG', None): import pydevd @@ -95,6 +96,8 @@ def git_logs(ctx, branch=None): def apiserver(ctx, port=8000, wait=True, autoreload=True, host='127.0.0.1', pty=True): """Run the API server.""" env = os.environ.copy() + from website import settings + cmd = 'DJANGO_SETTINGS_MODULE=api.base.settings {} manage.py runserver {}:{} --nothreading'\ .format(sys.executable, host, port) if not autoreload: @@ -114,6 +117,8 @@ def apiserver(ctx, port=8000, wait=True, autoreload=True, host='127.0.0.1', pty= def adminserver(ctx, port=8001, host='127.0.0.1', pty=True): """Run the Admin server.""" env = 'DJANGO_SETTINGS_MODULE="admin.base.settings"' + from website import settings + cmd = f'{env} python3 manage.py runserver {host}:{port} --nothreading' if settings.SECURE_MODE: cmd = cmd.replace('runserver', 'runsslserver') @@ -141,6 +146,7 @@ def sharejs(ctx, host=None, port=None, db_url=None, cors_allow_origin=None): os.environ['SHAREJS_DB_URL'] = db_url if cors_allow_origin: os.environ['SHAREJS_CORS_ALLOW_ORIGIN'] = cors_allow_origin + from website import settings if settings.SENTRY_DSN: os.environ['SHAREJS_SENTRY_DSN'] = settings.SENTRY_DSN @@ -179,7 +185,7 @@ def celery_beat(ctx, level='debug', schedule=None): ctx.run(bin_prefix(cmd), pty=True) @task -def migrate_search(ctx, delete=True, remove=False, index=settings.ELASTIC_INDEX): +def migrate_search(ctx, delete=True, remove=False, index='website'): """Migrate the search-enabled models.""" from website.app import init_app init_app(routes=False, set_backends=False) @@ -546,6 +552,7 @@ def wheelhouse(ctx, addons=False, release=False, dev=False, pty=True): inv wheelhouse --addons inv wheelhouse --release """ + from website import settings if release or addons: for directory in os.listdir(settings.ADDON_PATH): path = os.path.join(settings.ADDON_PATH, directory) @@ -567,6 +574,8 @@ def wheelhouse(ctx, addons=False, release=False, dev=False, pty=True): @task def addon_requirements(ctx): """Install all addon requirements.""" + from website import settings + for directory in os.listdir(settings.ADDON_PATH): path = os.path.join(settings.ADDON_PATH, directory) @@ -583,6 +592,8 @@ def addon_requirements(ctx): @task def ci_addon_settings(ctx): + from website import settings + for directory in os.listdir(settings.ADDON_PATH): path = os.path.join(settings.ADDON_PATH, directory, 'settings') if os.path.isdir(path): @@ -595,6 +606,8 @@ def ci_addon_settings(ctx): @task def copy_addon_settings(ctx): + from website import settings + for directory in os.listdir(settings.ADDON_PATH): path = os.path.join(settings.ADDON_PATH, directory, 'settings') if os.path.isdir(path) and not os.path.isfile(os.path.join(path, 'local.py')): diff --git a/website/notifications/utils.py b/website/notifications/utils.py index 83de52cdcaa..b9a9b6d10e3 100644 --- a/website/notifications/utils.py +++ b/website/notifications/utils.py @@ -72,7 +72,9 @@ def remove_contributor_from_subscriptions(node, user): if not (node.is_contributor_or_group_member(user)) and user._id not in node.admin_contributor_or_group_member_ids: node_subscriptions = get_all_node_subscriptions(user, node) for subscription in node_subscriptions: - subscription.remove_user_from_subscription(user) + subscription.objects.filter( + user=user, + ).delete() def separate_users(node, user_ids): """Separates users into ones with permissions and ones without given a list. @@ -131,7 +133,6 @@ def move_subscription(remove_users, source_event, source_node, new_event, new_no :return: Returns a NOTIFICATION_TYPES list of removed users without permissions """ NotificationSubscription = apps.get_model('osf.NotificationSubscription') - OSFUser = apps.get_model('osf.OSFUser') if source_node == new_node: return old_sub = NotificationSubscription.load(to_subscription_key(source_node._id, source_event)) @@ -150,8 +151,7 @@ def move_subscription(remove_users, source_event, source_node, new_event, new_no related_manager = getattr(new_sub, notification_type, None) subscriptions = related_manager.all() if related_manager else [] if user_id in subscriptions: - user = OSFUser.load(user_id) - new_sub.remove_user_from_subscription(user) + new_sub.delete() def get_configured_projects(user): diff --git a/website/notifications/views.py b/website/notifications/views.py index 2250e2f1e8a..700594f69d6 100644 --- a/website/notifications/views.py +++ b/website/notifications/views.py @@ -528,7 +528,7 @@ def configure_subscription(auth): if not subscription: return {} # We're done here - subscription.remove_user_from_subscription(user) + subscription.delete() return {} subscription, _ = NotificationSubscription.objects.get_or_create( 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 @@