diff --git a/CHANGELOG b/CHANGELOG index 1db53b2d592..f7b89a25aeb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,20 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. -24.01.0 (2024-04-30) +24.03.0 (2024-07-08) +==================== +- Preprints into Ember OSF Web, Phase 2 Backend Release + +24.03.0 (2024-07-08) +==================== +- Allow AbstractProviders to specify Discover page presence +- Update get_auth +- Expand Addons Service support functionality, waffled +- Set default Resource Type General for Registrations +- Bug fix: Archiver Unicode edge cases +- Bug fix: RelationshipField during project creation + +24.02.0 (2024-04-30) ==================== - Initial Addons Service work, Waffled - Improve default `description` for Google Dataset Discovery diff --git a/addons/base/views.py b/addons/base/views.py index 2e061f4f665..cafc2d58a83 100644 --- a/addons/base/views.py +++ b/addons/base/views.py @@ -166,7 +166,7 @@ def get_addon_user_config(**kwargs): return addon.to_json(user) - + def make_auth(user): if user is not None: return { @@ -207,9 +207,10 @@ def get_auth(auth, **kwargs): waterbutler_data = _decrypt_and_decode_jwt_payload() resource = _get_authenticated_resource(waterbutler_data['nid']) + action = waterbutler_data['action'] _check_resource_permissions(resource, auth, action) - + provider_name = waterbutler_data['provider'] file_version = file_node = None if provider_name == 'osfstorage': @@ -221,6 +222,7 @@ def get_auth(auth, **kwargs): resource=resource, provider_name=provider_name, file_version=file_version, ) + _enqueue_metrics(file_version=file_version, file_node=file_node, action=action, auth=auth) # Construct the response payload including the JWT @@ -230,8 +232,8 @@ def get_auth(auth, **kwargs): credentials=waterbutler_credentials, waterbutler_settings=waterbutler_settings ) - - + + def _decrypt_and_decode_jwt_payload(): try: payload_encrypted = request.args.get('payload', '').encode('utf-8') diff --git a/api/nodes/views.py b/api/nodes/views.py index e5d29a306ca..d49fd9eaf62 100644 --- a/api/nodes/views.py +++ b/api/nodes/views.py @@ -156,6 +156,7 @@ File, Folder, CedarMetadataRecord, + Preprint, ) from addons.osfstorage.models import Region from osf.utils.permissions import ADMIN, WRITE_NODE @@ -1514,6 +1515,8 @@ def id(self): @property def root_folder(self): + if isinstance(self.resource, Preprint): + return self.target.root_folder if self.provider_settings: return self.provider_settings.root_node return None diff --git a/api/preprints/serializers.py b/api/preprints/serializers.py index 6a00e581f4d..146490862af 100644 --- a/api/preprints/serializers.py +++ b/api/preprints/serializers.py @@ -111,6 +111,7 @@ class PreprintSerializer(TaxonomizableSerializerMixin, MetricsSerializerMixin, J date_modified = VersionedDateTimeField(source='modified', read_only=True) date_published = VersionedDateTimeField(read_only=True) original_publication_date = VersionedDateTimeField(required=False, allow_null=True) + custom_publication_citation = ser.CharField(required=False, allow_blank=True, allow_null=True) doi = ser.CharField(source='article_doi', required=False, allow_null=True) title = ser.CharField(required=True, max_length=512) description = ser.CharField(required=False, allow_blank=True, allow_null=True) @@ -222,6 +223,11 @@ def subjects_view_kwargs(self): # Overrides TaxonomizableSerializerMixin return {'preprint_id': '<_id>'} + @property + def subjects_self_view(self): + # Overrides TaxonomizableSerializerMixin + return 'preprints:preprint-relationships-subjects' + def get_preprint_url(self, obj): return absolute_reverse('preprints:preprint-detail', kwargs={'preprint_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version']}) @@ -326,6 +332,10 @@ def update(self, preprint, validated_data): preprint.original_publication_date = validated_data['original_publication_date'] or None save_preprint = True + if 'custom_publication_citation' in validated_data: + preprint.custom_publication_citation = validated_data['custom_publication_citation'] or None + save_preprint = True + if 'has_coi' in validated_data: try: preprint.update_has_coi(auth, validated_data['has_coi']) diff --git a/api/preprints/urls.py b/api/preprints/urls.py index 70c72d991f6..c8b48e6b1cd 100644 --- a/api/preprints/urls.py +++ b/api/preprints/urls.py @@ -16,6 +16,7 @@ re_path(r'^(?P\w+)/files/osfstorage/$', views.PreprintFilesList.as_view(), name=views.PreprintFilesList.view_name), re_path(r'^(?P\w+)/identifiers/$', views.PreprintIdentifierList.as_view(), name=views.PreprintIdentifierList.view_name), re_path(r'^(?P\w+)/relationships/node/$', views.PreprintNodeRelationship.as_view(), name=views.PreprintNodeRelationship.view_name), + re_path(r'^(?P\w+)/relationships/subjects/$', views.PreprintSubjectsRelationship.as_view(), name=views.PreprintSubjectsRelationship.view_name), re_path(r'^(?P\w+)/review_actions/$', views.PreprintActionList.as_view(), name=views.PreprintActionList.view_name), re_path(r'^(?P\w+)/requests/$', views.PreprintRequestListCreate.as_view(), name=views.PreprintRequestListCreate.view_name), re_path(r'^(?P\w+)/subjects/$', views.PreprintSubjectsList.as_view(), name=views.PreprintSubjectsList.view_name), diff --git a/api/preprints/views.py b/api/preprints/views.py index 08df330c7db..302b8623102 100644 --- a/api/preprints/views.py +++ b/api/preprints/views.py @@ -58,7 +58,7 @@ from api.requests.permissions import PreprintRequestPermission from api.requests.serializers import PreprintRequestSerializer, PreprintRequestCreateSerializer from api.requests.views import PreprintRequestMixin -from api.subjects.views import BaseResourceSubjectsList +from api.subjects.views import BaseResourceSubjectsList, SubjectRelationshipBaseView from api.base.metrics import PreprintMetricsViewMixin from osf.metrics import PreprintDownload, PreprintView @@ -456,6 +456,37 @@ class PreprintSubjectsList(BaseResourceSubjectsList, PreprintMixin): def get_resource(self): return self.get_preprint() + +class PreprintSubjectsRelationship(SubjectRelationshipBaseView, PreprintMixin): + """The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/preprint_subjects_list). + """ + permission_classes = ( + drf_permissions.IsAuthenticatedOrReadOnly, + base_permissions.TokenHasScope, + ModeratorIfNeverPublicWithdrawn, + ContributorOrPublic, + PreprintPublishedOrWrite, + ) + + required_read_scopes = [CoreScopes.PREPRINTS_READ] + required_write_scopes = [CoreScopes.PREPRINTS_WRITE] + + view_category = 'preprints' + view_name = 'preprint-relationships-subjects' + + def get_resource(self, check_object_permissions=True): + return self.get_preprint(check_object_permissions=check_object_permissions) + + def get_object(self): + resource = self.get_resource(check_object_permissions=False) + obj = { + 'data': resource.subjects.all(), + 'self': resource, + } + self.check_object_permissions(self.request, resource) + return obj + + class PreprintActionList(JSONAPIBaseView, generics.ListCreateAPIView, ListFilterMixin, PreprintMixin): """Action List *Read-only* diff --git a/api_tests/files/views/test_file_detail.py b/api_tests/files/views/test_file_detail.py index 14b95016e36..7b40be30d4c 100644 --- a/api_tests/files/views/test_file_detail.py +++ b/api_tests/files/views/test_file_detail.py @@ -31,6 +31,9 @@ SessionStore = import_module(django_conf_settings.SESSION_ENGINE).SessionStore +from addons.base.views import _get_authenticated_resource +from framework.exceptions import HTTPError + # stolen from^W^Winspired by DRF # rest_framework.fields.DateTimeField.to_representation def _dt_to_iso8601(value): @@ -705,6 +708,48 @@ def test_load_and_property(self, app, user, file): expect_errors=True, auth=user.auth, ).status_code == 405 + def test_retracted_file_returns_410(self, app, user, file_url, file): + resource = RegistrationFactory(is_public=True) + retraction = resource.retract_registration( + user=resource.creator, + justification='Justification for retraction', + save=True, + moderator_initiated=False + ) + + retraction.accept() + resource.save() + resource.refresh_from_db() + + file.target = resource + file.save() + + res = app.get(file_url, auth=user.auth, expect_errors=True) + assert res.status_code == 410 + + def test_get_authenticated_resource_retracted(self): + resource = RegistrationFactory(is_public=True) + + assert resource.is_retracted is False + + retraction = resource.retract_registration( + user=resource.creator, + justification='Justification for retraction', + save=True, + moderator_initiated=False + ) + + retraction.accept() + resource.save() + resource.refresh_from_db() + + assert resource.is_retracted is True + + with pytest.raises(HTTPError) as excinfo: + _get_authenticated_resource(resource._id) + + assert excinfo.value.code == 410 + @pytest.mark.django_db class TestFileTagging: diff --git a/api_tests/preprints/views/test_preprint_detail.py b/api_tests/preprints/views/test_preprint_detail.py index aca17d6901e..332aea74a6c 100644 --- a/api_tests/preprints/views/test_preprint_detail.py +++ b/api_tests/preprints/views/test_preprint_detail.py @@ -305,6 +305,19 @@ def test_update_original_publication_date_to_none(self, app, preprint, url): preprint.reload() assert preprint.original_publication_date is None + def test_update_custom_publication_citation_to_none(self, app, preprint, url): + write_contrib = AuthUserFactory() + preprint.add_contributor(write_contrib, WRITE, save=True) + preprint.custom_publication_citation = 'fake citation' + preprint.save() + update_payload = build_preprint_update_payload( + preprint._id, attributes={'custom_publication_citation': None} + ) + res = app.patch_json_api(url, update_payload, auth=write_contrib.auth) + assert res.status_code == 200 + preprint.reload() + assert preprint.custom_publication_citation is None + @responses.activate @mock.patch('osf.models.preprint.update_or_enqueue_on_preprint_updated', mock.Mock()) def test_update_preprint_permission_write_contrib(self, app, preprint, url): @@ -536,6 +549,16 @@ def test_update_original_publication_date(self, app, user, preprint, url): preprint.reload() assert preprint.original_publication_date == date + def test_update_custom_publication_citation(self, app, user, preprint, url): + citation = 'fake citation' + update_payload = build_preprint_update_payload( + preprint._id, attributes={'custom_publication_citation': citation} + ) + res = app.patch_json_api(url, update_payload, auth=user.auth) + assert res.status_code == 200 + preprint.reload() + assert preprint.custom_publication_citation == citation + @responses.activate @mock.patch('osf.models.preprint.update_or_enqueue_on_preprint_updated', mock.Mock()) def test_update_article_doi(self, app, user, preprint, url): diff --git a/osf/external/gravy_valet/translations.py b/osf/external/gravy_valet/translations.py index a004515002b..2fae78e1e98 100644 --- a/osf/external/gravy_valet/translations.py +++ b/osf/external/gravy_valet/translations.py @@ -117,7 +117,6 @@ def create_waterbutler_log(self, *args, **kwargs): def save(self): pass - @dataclasses.dataclass class EphemeralUserSettings: '''Minimalist dataclass for storing the actually used properties of UserSettings.''' diff --git a/osf/migrations/0021_preprint_custom_publication_citation.py b/osf/migrations/0021_preprint_custom_publication_citation.py new file mode 100644 index 00000000000..678d3bd2192 --- /dev/null +++ b/osf/migrations/0021_preprint_custom_publication_citation.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2024-05-15 15:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0020_abstractprovider_advertise_on_discover_page'), + ] + + operations = [ + migrations.AddField( + model_name='preprint', + name='custom_publication_citation', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/osf/models/mixins.py b/osf/models/mixins.py index fe8642f2a99..7944ab662d7 100644 --- a/osf/models/mixins.py +++ b/osf/models/mixins.py @@ -3,6 +3,8 @@ import markupsafe import logging +import waffle + from django.apps import apps from django.contrib.auth.models import Group, AnonymousUser from django.core.exceptions import ObjectDoesNotExist, ValidationError diff --git a/osf/models/preprint.py b/osf/models/preprint.py index c52812860e4..43f7a56b2ae 100644 --- a/osf/models/preprint.py +++ b/osf/models/preprint.py @@ -152,6 +152,7 @@ class Preprint(DirtyFieldsMixin, GuidMixin, IdentifierMixin, ReviewableMixin, Ba is_published = models.BooleanField(default=False, db_index=True) date_published = NonNaiveDateTimeField(null=True, blank=True) original_publication_date = NonNaiveDateTimeField(null=True, blank=True) + custom_publication_citation = models.TextField(null=True, blank=True) license = models.ForeignKey('osf.NodeLicenseRecord', on_delete=models.SET_NULL, null=True, blank=True) diff --git a/package.json b/package.json index d95254712c5..e23665cd5f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "OSF", - "version": "24.02.0", + "version": "24.04.0", "description": "Facilitating Open Science", "repository": "https://github.com/CenterForOpenScience/osf.io", "author": "Center for Open Science", diff --git a/website/identifiers/tasks.py b/website/identifiers/tasks.py index 2fd51428dc3..f940956d54b 100644 --- a/website/identifiers/tasks.py +++ b/website/identifiers/tasks.py @@ -5,12 +5,15 @@ from framework.celery_tasks.handlers import queued_task from framework import sentry - -@queued_task -@celery_app.task(ignore_results=True) -def update_doi_metadata_on_change(target_guid): +@celery_app.task(bind=True, max_retries=5, acks_late=True) +def task__update_doi_metadata_on_change(self, target_guid): sentry.log_message('Updating DOI for guid', extra_data={'guid': target_guid}, level=logging.INFO) Guid = apps.get_model('osf.Guid') target_object = Guid.load(target_guid).referent if target_object.get_identifier('doi'): target_object.request_identifier_update(category='doi') + +@queued_task +@celery_app.task(ignore_results=True) +def update_doi_metadata_on_change(target_guid): + task__update_doi_metadata_on_change(target_guid) diff --git a/website/routes.py b/website/routes.py index 787fe2e367b..eb3f93fd186 100644 --- a/website/routes.py +++ b/website/routes.py @@ -261,15 +261,7 @@ def ember_app(path=None): if request.path.strip('/').startswith(k): ember_app = EXTERNAL_EMBER_APPS[k] if k == 'preprints': - if request.path.rstrip('/').endswith('edit'): - # Route preprint edit pages to old preprint app - ember_app = EXTERNAL_EMBER_APPS.get('preprints', False) or ember_app - elif request.path.rstrip('/').endswith('submit'): - # Route preprint submit pages to old preprint app - ember_app = EXTERNAL_EMBER_APPS.get('preprints', False) or ember_app - else: - # Route other preprint pages to EOW - ember_app = EXTERNAL_EMBER_APPS.get('ember_osf_web', False) or ember_app + ember_app = EXTERNAL_EMBER_APPS.get('ember_osf_web', False) or ember_app break if not ember_app: diff --git a/website/settings/defaults.py b/website/settings/defaults.py index ce44070357b..79200467839 100644 --- a/website/settings/defaults.py +++ b/website/settings/defaults.py @@ -455,7 +455,7 @@ class CeleryConfig: 'website.mailchimp_utils', 'website.notifications.tasks', 'website.collections.tasks', - 'website.identifier.tasks', + 'website.identifiers.tasks', 'website.preprints.tasks', 'website.project.tasks', } @@ -516,6 +516,7 @@ class CeleryConfig: 'website.mailchimp_utils', 'website.notifications.tasks', 'website.archiver.tasks', + 'website.identifiers.tasks', 'website.search.search', 'website.project.tasks', 'scripts.populate_new_and_noteworthy_projects', diff --git a/website/views.py b/website/views.py index 9de84299652..de61c5548e6 100644 --- a/website/views.py +++ b/website/views.py @@ -37,7 +37,6 @@ from api.waffle.utils import storage_i18n_flag_active, flag_is_active logger = logging.getLogger(__name__) -preprints_dir = os.path.abspath(os.path.join(os.getcwd(), EXTERNAL_EMBER_APPS['preprints']['path'])) ember_osf_web_dir = os.path.abspath(os.path.join(os.getcwd(), EXTERNAL_EMBER_APPS['ember_osf_web']['path'])) @@ -331,8 +330,6 @@ def resolve_guid(guid, suffix=None): if isinstance(resource, Preprint): if resource.provider.domain_redirect_enabled: return redirect(resource.absolute_url, http_status.HTTP_301_MOVED_PERMANENTLY) - if clean_suffix.endswith('edit'): - return stream_emberapp(EXTERNAL_EMBER_APPS['preprints']['server'], preprints_dir) return use_ember_app() elif isinstance(resource, Registration) and (clean_suffix in ('', 'comments', 'links', 'components', 'resources',)) and flag_is_active(request, features.EMBER_REGISTRIES_DETAIL_PAGE):