From 8ce1dfb5025835f0bf38d41f5e753675e0e5f353 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Tue, 8 Apr 2025 10:04:05 -0400 Subject: [PATCH 1/9] [ENG-7761] Edit scopes with Personal Access Tokens (#11081) ## Purpose Allow integrator to change the scope of their PAT. So that full token read/write scopes fall under full_write scope. ## Changes - clean of test_token_detail test, splitting them up into files <500 ln - Add a test for editing PAT scopes with a PAT - Add permission to make possible: https://github.com/CenterForOpenScience/osf.io/pull/11070/files#r2025067543 ## Ticket https://openscience.atlassian.net/browse/ENG-7761 --- api_tests/tokens/views/test_token_detail.py | 722 ++++++++---------- .../views/test_token_detail_relationships.py | 495 ++++++++++++ framework/auth/oauth_scopes.py | 1 + 3 files changed, 804 insertions(+), 414 deletions(-) create mode 100644 api_tests/tokens/views/test_token_detail_relationships.py diff --git a/api_tests/tokens/views/test_token_detail.py b/api_tests/tokens/views/test_token_detail.py index 0f441efc108..c46a0dd6f8a 100644 --- a/api_tests/tokens/views/test_token_detail.py +++ b/api_tests/tokens/views/test_token_detail.py @@ -2,6 +2,7 @@ import pytest from api.scopes.serializers import SCOPES_RELATIONSHIP_VERSION +from framework.auth.cas import CasResponse from osf_tests.factories import ( ApiOAuth2PersonalTokenFactory, ApiOAuth2ScopeFactory, @@ -10,36 +11,28 @@ from tests.base import assert_dict_contains_subset from website.util import api_v2_url + @pytest.mark.django_db -def post_payload( - type_payload='tokens', - scopes=None, - name='A shiny updated token'): - if not scopes: - scopes = ApiOAuth2ScopeFactory().name - return { - 'data': { - 'type': type_payload, - 'attributes': { - 'name': name, - }, - 'relationships': { - 'scopes': { - 'data': [ - { - 'type': 'scopes', - 'id': scopes - } - ] +class TestTokenDetailScopesAsAttributes: + def post_attributes_payload(self, type_payload='tokens', scopes='osf.full_write', name='A shiny updated token'): + return { + 'data': { + 'type': type_payload, + 'attributes': { + 'name': name, + 'scopes': scopes, } } } - } + @pytest.fixture() + def write_scope(self): + return ApiOAuth2ScopeFactory(name='osf.full_write') -@pytest.mark.django_db -class TestTokenDetailScopesAsRelationships: + @pytest.fixture() + def read_scope(self): + return ApiOAuth2ScopeFactory(name='osf.full_read') @pytest.fixture() def user_one(self): @@ -53,46 +46,85 @@ def user_two(self): def token_user_one(self, user_one): return ApiOAuth2PersonalTokenFactory(owner=user_one) + @pytest.fixture() + def token_full_write(self, user_one, write_scope): + token = ApiOAuth2PersonalTokenFactory( + owner=user_one, + ) + token.scopes.add(write_scope) + return token + @pytest.fixture() def url_token_detail(self, user_one, token_user_one): path = f'tokens/{token_user_one._id}/?version={SCOPES_RELATIONSHIP_VERSION}' return api_v2_url(path, base_route='/') @pytest.fixture() - def url_token_list(self): - return api_v2_url(f'tokens/?version={SCOPES_RELATIONSHIP_VERSION}', base_route='/') + def url_token_detail_full_write(self, user_one, token_full_write): + path = f'tokens/{token_full_write._id}/' + return api_v2_url(path, base_route='/') @pytest.fixture() - def read_scope(self): - return ApiOAuth2ScopeFactory() - - def test_token_detail_who_can_view( - self, app, url_token_detail, user_one, - user_two, token_user_one): + def url_token_list(self): + return api_v2_url('tokens/', base_route='/') - # test_owner_can_view + def test_owner_can_view( + self, + app, + url_token_detail, + user_one, + user_two, + token_user_one + ): res = app.get(url_token_detail, auth=user_one.auth) assert res.status_code == 200 assert res.json['data']['id'] == token_user_one._id - # test_non_owner_cant_view + def test_non_owner_cant_view( + self, + app, + url_token_detail, + user_one, + user_two, + token_user_one + ): res = app.get(url_token_detail, auth=user_two.auth, expect_errors=True) assert res.status_code == 403 - # test_returns_401_when_not_logged_in + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_returns_401_when_not_logged_in( + self, + mock_method, + app, + url_token_detail, + user_one, + user_two, + token_user_one + ): + mock_method.return_value(True) res = app.get(url_token_detail, expect_errors=True) assert res.status_code == 401 @mock.patch('framework.auth.cas.CasClient.revoke_tokens') def test_owner_can_delete( - self, mock_method, app, user_one, url_token_detail): + self, + mock_method, + app, + user_one, + url_token_detail + ): mock_method.return_value(True) res = app.delete(url_token_detail, auth=user_one.auth) assert res.status_code == 204 @mock.patch('framework.auth.cas.CasClient.revoke_tokens') def test_deleting_tokens_makes_api_view_inaccessible( - self, mock_method, app, url_token_detail, user_one): + self, + mock_method, + app, + url_token_detail, + user_one + ): mock_method.return_value(True) app.delete(url_token_detail, auth=user_one.auth) res = app.get(url_token_detail, auth=user_one.auth, expect_errors=True) @@ -100,9 +132,16 @@ def test_deleting_tokens_makes_api_view_inaccessible( @mock.patch('framework.auth.cas.CasClient.revoke_tokens') def test_updating_one_field_should_not_blank_others_on_patch_update( - self, mock_revoke, app, token_user_one, url_token_detail, user_one, read_scope): - mock_revoke.return_value = True - user_one_token = token_user_one + self, + mock_method, + app, + url_token_detail, + token_user_one, + user_one, + write_scope, + read_scope + ): + mock_method.return_value(True) new_name = 'The token formerly known as Prince' res = app.patch_json_api( url_token_detail, @@ -126,499 +165,354 @@ def test_updating_one_field_should_not_blank_others_on_patch_update( } } }, - auth=user_one.auth) - user_one_token.reload() + auth=user_one.auth + ) + token_user_one.reload() assert res.status_code == 200 - assert_dict_contains_subset( { 'name': new_name, }, res.json['data']['attributes']) - assert res.json['data']['id'] == user_one_token._id - assert res.json['data']['relationships']['owner']['data']['id'] == user_one_token.owner._id + assert res.json['data']['id'] == token_user_one._id + assert res.json['data']['relationships']['owner']['data']['id'] == token_user_one.owner._id assert len(res.json['data']['embeds']['scopes']['data']) == 1 assert res.json['data']['embeds']['scopes']['data'][0]['id'] == read_scope.name @mock.patch('framework.auth.cas.CasClient.revoke_tokens') def test_updating_an_instance_does_not_change_the_number_of_instances( - self, mock_revoke, app, url_token_detail, url_token_list, token_user_one, user_one, read_scope): - mock_revoke.return_value = True - new_name = 'The token formerly known as Prince' + self, + mock_method, + app, + url_token_detail, + url_token_list, + token_user_one, + user_one, + write_scope + ): + mock_method.return_value(True) res = app.patch_json_api( url_token_detail, - {'data': { - 'attributes': { - 'name': new_name, - }, - 'relationships': { - 'scopes': { - 'data': [ - { - 'type': 'scopes', - 'id': read_scope.name - } - ] - - } - }, - 'id': token_user_one._id, - 'type': 'tokens'}}, - auth=user_one.auth) + { + 'data': { + 'attributes': { + 'name': 'The token formerly known as Prince', + 'scopes': 'osf.full_write' + }, + 'id': token_user_one._id, + 'type': 'tokens' + } + }, + auth=user_one.auth + ) assert res.status_code == 200 res = app.get(url_token_list, auth=user_one.auth) assert res.status_code == 200 assert (len(res.json['data']) == 1) + assert res.json['data'][0]['attributes']['scopes'] == write_scope.name @mock.patch('framework.auth.cas.CasClient.revoke_tokens') def test_deleting_token_flags_instance_inactive( - self, mock_method, app, url_token_detail, user_one, token_user_one): + self, + mock_method, + app, + url_token_detail, + user_one, + token_user_one + ): mock_method.return_value(True) app.delete(url_token_detail, auth=user_one.auth) token_user_one.reload() assert not token_user_one.is_active - def test_read_does_not_return_token_id( - self, app, url_token_detail, user_one): + def test_read_does_not_return_token_id(self, app, url_token_detail, user_one): res = app.get(url_token_detail, auth=user_one.auth) assert res.status_code == 200 assert 'token_id' not in res.json['data']['attributes'] @mock.patch('framework.auth.cas.CasClient.revoke_tokens') def test_update_token_does_not_return_token_id( - self, mock_revoke, app, url_token_detail, user_one): - mock_revoke.return_value = True - correct = post_payload() + self, + mock_method, + app, + url_token_detail, + user_one, + write_scope + ): + mock_method.return_value(True) res = app.put_json_api( url_token_detail, - correct, + self.post_attributes_payload(scopes='osf.full_write'), auth=user_one.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 200 assert 'token_id' not in res.json['data']['attributes'] @mock.patch('framework.auth.cas.CasClient.revoke_tokens') - def test_update_token(self, mock_revoke, app, user_one, url_token_detail): - mock_revoke.return_value = True - correct = post_payload() - res = app.put_json_api( + def test_update_token( + self, + mock_method, + app, + user_one, + token_user_one, url_token_detail, - correct, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 200 - - @mock.patch('framework.auth.cas.CasClient.revoke_tokens') - def test_update_token_add_scope(self, mock_revoke, app, user_one, token_user_one, url_token_detail): - mock_revoke.return_value = True - original_scope = token_user_one.scopes.first() - scope = ApiOAuth2ScopeFactory() - correct = post_payload(scopes=scope.name) + write_scope, + ): + mock_method.return_value(True) res = app.put_json_api( url_token_detail, - correct, + self.post_attributes_payload(scopes=write_scope.name), auth=user_one.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 200 - scopes_data = res.json['data']['embeds']['scopes']['data'] - assert len(scopes_data) == 1 - assert scopes_data[0]['id'] == scope.name - assert scope.name != original_scope.name + assert res.json['data']['relationships']['scopes'] + token_user_one.reload() + assert token_user_one.scopes.all().first().name == write_scope.name - @pytest.mark.enable_implicit_clean @mock.patch('framework.auth.cas.CasClient.revoke_tokens') - def test_token_detail_crud_with_wrong_payload( - self, mock_revoke, app, url_token_list, url_token_detail, - token_user_one, user_one, user_two): - mock_revoke.return_value = True - - # test_non_owner_cant_delete - res = app.delete( + @mock.patch('framework.auth.cas.CasClient.profile') + def test_owner_can_edit_scopes_via_pat( + self, + mock_cas, + mock_method, + app, + user_one, + token_user_one, + token_full_write, url_token_detail, - auth=user_two.auth, - expect_errors=True) - assert res.status_code == 403 - - # test_create_with_nonexistant_scope_fails - injected_scope = post_payload( - name='A shiny invalid token', - scopes='osf.admin') - res = app.post_json_api( - url_token_list, - injected_scope, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 404 - - # test_create_with_private_scope_fails - fake_scope = ApiOAuth2ScopeFactory() - fake_scope.is_public = False - fake_scope.save() - nonsense_scope = post_payload( - name='A shiny invalid token', - scopes=fake_scope.name) - res = app.post_json_api( - url_token_list, - nonsense_scope, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 400 - - # test_update_with_nonexistant_scope_fails - injected_scope = post_payload( - name='A shiny invalid token', - scopes='osf.admin') - res = app.put_json_api( - url_token_detail, - injected_scope, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 404 - - # test_update_with_private_scope_fails - nonsense_scope = post_payload( - name='A shiny invalid token', - scopes=fake_scope.name) - res = app.put_json_api( - url_token_detail, - nonsense_scope, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 400 - - # test_update_token_incorrect_type - incorrect_type = post_payload(type_payload='Wrong type.') - res = app.put_json_api( - url_token_detail, - incorrect_type, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 409 - - # test_update_token_no_type - missing_type = post_payload(type_payload='') - res = app.put_json_api( - url_token_detail, - missing_type, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 400 - - # test_update_token_no_attributes - payload = { - 'id': token_user_one._id, - 'type': 'tokens', - 'name': 'The token formerly known as Prince' - } - res = app.put_json_api( - url_token_detail, - payload, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 400 - - # test_partial_update_token_incorrect_type - incorrect_type = post_payload(type_payload='Wrong type.') - res = app.patch_json_api( - url_token_detail, - incorrect_type, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 409 - - # test_partial_update_token_no_type - missing_type = post_payload(type_payload='') - res = app.patch_json_api( - url_token_detail, - missing_type, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 400 - - -def post_attributes_payload( - type_payload='tokens', - scopes='osf.full_write', - name='A shiny updated token'): - return { - 'data': { - 'type': type_payload, - 'attributes': { - 'name': name, - 'scopes': scopes, - } - } - } - - -@pytest.mark.django_db -class TestTokenDetailScopesAsAttributes: - - @pytest.fixture() - def write_scope(self): - return ApiOAuth2ScopeFactory(name='osf.full_write') - - @pytest.fixture() - def user_one(self): - return AuthUserFactory() - - @pytest.fixture() - def user_two(self): - return AuthUserFactory() - - @pytest.fixture() - def token_user_one(self, user_one): - return ApiOAuth2PersonalTokenFactory(owner=user_one) - - @pytest.fixture() - def url_token_detail(self, user_one, token_user_one): - path = f'tokens/{token_user_one._id}/' - return api_v2_url(path, base_route='/') - - @pytest.fixture() - def url_token_list(self): - return api_v2_url('tokens/', base_route='/') - - def test_token_detail_who_can_view( - self, app, url_token_detail, user_one, - user_two, token_user_one): - - # test_owner_can_view - res = app.get(url_token_detail, auth=user_one.auth) - assert res.status_code == 200 - assert res.json['data']['id'] == token_user_one._id - assert res.json['data']['attributes']['scopes'] == token_user_one.scopes.first().name - - # test_non_owner_cant_view - res = app.get(url_token_detail, auth=user_two.auth, expect_errors=True) - assert res.status_code == 403 - - # test_returns_401_when_not_logged_in - res = app.get(url_token_detail, expect_errors=True) - assert res.status_code == 401 - - @mock.patch('framework.auth.cas.CasClient.revoke_tokens') - def test_owner_can_delete( - self, mock_method, app, user_one, url_token_detail): - mock_method.return_value(True) - res = app.delete(url_token_detail, auth=user_one.auth) - assert res.status_code == 204 - - @mock.patch('framework.auth.cas.CasClient.revoke_tokens') - def test_deleting_tokens_makes_api_view_inaccessible( - self, mock_method, app, url_token_detail, user_one): + write_scope + ): mock_method.return_value(True) - app.delete(url_token_detail, auth=user_one.auth) - res = app.get(url_token_detail, auth=user_one.auth, expect_errors=True) - assert res.status_code == 404 + mock_cas.return_value = CasResponse( + authenticated=True, + user=user_one._id, + attributes={'accessTokenScope': [write_scope.name,]} + ) - @mock.patch('framework.auth.cas.CasClient.revoke_tokens') - def test_updating_one_field_should_not_blank_others_on_patch_update( - self, mock_revoke, app, token_user_one, url_token_detail, user_one, write_scope): - mock_revoke.return_value = True - user_one_token = token_user_one - new_name = 'The token formerly known as Prince' - res = app.patch_json_api( + res = app.put_json_api( url_token_detail, { 'data': { 'attributes': { - 'name': new_name, - 'scopes': 'osf.full_write' + 'name': token_user_one.name, + 'scopes': write_scope.name }, - 'id': token_user_one._id, + 'id': token_full_write._id, 'type': 'tokens' } }, - auth=user_one.auth) - user_one_token.reload() - assert res.status_code == 200 - assert_dict_contains_subset( - { - 'owner': user_one_token.owner._id, - 'name': new_name, - 'scopes': f'{write_scope.name}', - }, - res.json['data']['attributes']) - assert res.json['data']['id'] == user_one_token._id - - @mock.patch('framework.auth.cas.CasClient.revoke_tokens') - def test_updating_an_instance_does_not_change_the_number_of_instances( - self, mock_revoke, app, url_token_detail, url_token_list, token_user_one, user_one, write_scope): - mock_revoke.return_value = True - new_name = 'The token formerly known as Prince' - res = app.patch_json_api( - url_token_detail, - {'data': { - 'attributes': { - 'name': new_name, - 'scopes': 'osf.full_write' - }, - 'id': token_user_one._id, - 'type': 'tokens'}}, - auth=user_one.auth) - assert res.status_code == 200 - - res = app.get(url_token_list, auth=user_one.auth) - assert res.status_code == 200 - assert (len(res.json['data']) == 1) - assert res.json['data'][0]['attributes']['scopes'] == write_scope.name - - @mock.patch('framework.auth.cas.CasClient.revoke_tokens') - def test_deleting_token_flags_instance_inactive( - self, mock_method, app, url_token_detail, user_one, token_user_one): - mock_method.return_value(True) - app.delete(url_token_detail, auth=user_one.auth) - token_user_one.reload() - assert not token_user_one.is_active - - def test_read_does_not_return_token_id( - self, app, url_token_detail, user_one): - res = app.get(url_token_detail, auth=user_one.auth) + headers={'Authorization': f'Bearer {token_full_write.token_id}'}, + ) assert res.status_code == 200 - assert 'token_id' not in res.json['data']['attributes'] - assert 'scopes' in res.json['data']['attributes'] + token_full_write.refresh_from_db() + assert write_scope in token_full_write.scopes.all() - @mock.patch('framework.auth.cas.CasClient.revoke_tokens') - def test_update_token_does_not_return_token_id( - self, mock_revoke, app, url_token_detail, user_one, write_scope): - mock_revoke.return_value = True - correct = post_attributes_payload(scopes='osf.full_write') - res = app.put_json_api( - url_token_detail, - correct, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 200 - assert 'token_id' not in res.json['data']['attributes'] - assert res.json['data']['attributes']['scopes'] == write_scope.name - - @mock.patch('framework.auth.cas.CasClient.revoke_tokens') - def test_update_token(self, mock_revoke, app, user_one, url_token_detail, write_scope): - mock_revoke.return_value = True - correct = post_attributes_payload(scopes='osf.full_write') - res = app.put_json_api( + def test_non_owner_cant_delete( + self, + app, + url_token_list, url_token_detail, - correct, - auth=user_one.auth, - expect_errors=True) - assert res.status_code == 200 - assert res.json['data']['attributes']['scopes'] == write_scope.name - - def test_token_detail_crud_with_wrong_payload( - self, app, url_token_list, url_token_detail, - token_user_one, user_one, user_two): - - # test_non_owner_cant_delete + token_user_one, + user_one, + user_two + ): res = app.delete( url_token_detail, auth=user_two.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 403 - # test_create_with_admin_scope_fails + def test_create_with_admin_scope_fails( + self, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): admin_token = ApiOAuth2ScopeFactory(name='osf.admin') admin_token.is_public = False admin_token.save() - injected_scope = post_attributes_payload( + injected_scope = self.post_attributes_payload( name='A shiny invalid token', - scopes='osf.admin') + scopes='osf.admin' + ) res = app.post_json_api( url_token_list, injected_scope, auth=user_one.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 400 - # test_create_with_fake_scope_fails - nonsense_scope = post_attributes_payload( + def test_create_with_fake_scope_fails( + self, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): + nonsense_scope = self.post_attributes_payload( name='A shiny invalid token', - scopes='osf.nonsense') + scopes='osf.nonsense' + ) res = app.post_json_api( url_token_list, nonsense_scope, auth=user_one.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 404 - # test_update_with_admin_scope_fails - injected_scope = post_attributes_payload( - name='A shiny invalid token', - scopes='osf.admin') + def test_update_with_admin_scope_fails( + self, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): res = app.put_json_api( url_token_detail, - injected_scope, + self.post_attributes_payload( + name='A shiny invalid token', + scopes='osf.admin' + ), auth=user_one.auth, - expect_errors=True) - assert res.status_code == 400 + expect_errors=True + ) + assert res.status_code == 404 - # test_update_with_fake_scope_fails - nonsense_scope = post_attributes_payload( - name='A shiny invalid token', - scopes='osf.nonsense') + def test_update_with_fake_scope_fails( + self, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): res = app.put_json_api( url_token_detail, - nonsense_scope, + self.post_attributes_payload( + name='A shiny invalid token', + scopes='osf.nonsense' + ), auth=user_one.auth, expect_errors=True) assert res.status_code == 404 - # test_update_token_incorrect_type - incorrect_type = post_attributes_payload(type_payload='Wrong type.') + def test_update_token_incorrect_type( + self, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): res = app.put_json_api( url_token_detail, - incorrect_type, + self.post_attributes_payload(type_payload='Wrong type.'), auth=user_one.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 409 - # test_update_token_no_type - missing_type = post_attributes_payload(type_payload='') + def test_update_token_no_type( + self, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): res = app.put_json_api( url_token_detail, - missing_type, + self.post_attributes_payload(type_payload=''), auth=user_one.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 400 - # test_update_token_no_attributes - payload = { - 'id': token_user_one._id, - 'type': 'tokens', - 'name': 'The token formerly known as Prince' - } + def test_update_token_no_attributes( + self, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): res = app.put_json_api( url_token_detail, - payload, + { + 'id': token_user_one._id, + 'type': 'tokens', + 'name': 'The token formerly known as Prince' + }, auth=user_one.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 400 - # test_partial_update_token_incorrect_type - incorrect_type = post_attributes_payload(type_payload='Wrong type.') + def test_partial_update_token_incorrect_type( + self, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): res = app.patch_json_api( url_token_detail, - incorrect_type, + self.post_attributes_payload(type_payload='Wrong type.'), auth=user_one.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 409 - # test_partial_update_token_no_type - missing_type = post_attributes_payload(type_payload='') + def test_partial_update_token_no_type( + self, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): res = app.patch_json_api( url_token_detail, - missing_type, + self.post_attributes_payload(type_payload=''), auth=user_one.auth, - expect_errors=True) + expect_errors=True + ) assert res.status_code == 400 - # test token too long - payload = post_payload(name='A' * 101) + def test_token_too_long( + self, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): res = app.put_json_api( url_token_detail, - payload, + self.post_attributes_payload(name='A' * 101), auth=user_one.auth, - expect_errors=True) - assert res.status_code == 400 + expect_errors=True + ) + assert res.status_code == 404 diff --git a/api_tests/tokens/views/test_token_detail_relationships.py b/api_tests/tokens/views/test_token_detail_relationships.py new file mode 100644 index 00000000000..b8f3c01de6a --- /dev/null +++ b/api_tests/tokens/views/test_token_detail_relationships.py @@ -0,0 +1,495 @@ +from unittest import mock +import pytest + +from api.scopes.serializers import SCOPES_RELATIONSHIP_VERSION +from osf_tests.factories import ( + ApiOAuth2PersonalTokenFactory, + ApiOAuth2ScopeFactory, + AuthUserFactory, +) +from tests.base import assert_dict_contains_subset +from website.util import api_v2_url + +@pytest.mark.django_db +class TestTokenDetailScopesAsRelationships: + + def post_payload(self, type_payload='tokens', scopes=None, name='A shiny updated token'): + if not scopes: + scopes = ApiOAuth2ScopeFactory().name + + return { + 'data': { + 'type': type_payload, + 'attributes': { + 'name': name, + }, + 'relationships': { + 'scopes': { + 'data': [ + { + 'type': 'scopes', + 'id': scopes + } + ] + } + } + } + } + + @pytest.fixture() + def user_one(self): + return AuthUserFactory() + + @pytest.fixture() + def user_two(self): + return AuthUserFactory() + + @pytest.fixture() + def token_user_one(self, user_one): + return ApiOAuth2PersonalTokenFactory(owner=user_one) + + @pytest.fixture() + def url_token_detail(self, user_one, token_user_one): + path = f'tokens/{token_user_one._id}/?version={SCOPES_RELATIONSHIP_VERSION}' + return api_v2_url(path, base_route='/') + + @pytest.fixture() + def url_token_list(self): + return api_v2_url(f'tokens/?version={SCOPES_RELATIONSHIP_VERSION}', base_route='/') + + @pytest.fixture() + def read_scope(self): + return ApiOAuth2ScopeFactory(name='osf.full_read') + + @pytest.fixture() + def private_scope(self): + return ApiOAuth2ScopeFactory(is_public=False) + + def test_owner_can_view( + self, + app, + url_token_detail, + user_one, + user_two, + token_user_one + ): + res = app.get(url_token_detail, auth=user_one.auth) + assert res.status_code == 200 + assert res.json['data']['id'] == token_user_one._id + + def test_non_owner_cant_view( + self, + app, + url_token_detail, + user_one, + user_two, + token_user_one + ): + res = app.get(url_token_detail, auth=user_two.auth, expect_errors=True) + assert res.status_code == 403 + + def test_returns_401_when_not_logged_in( + self, + app, + url_token_detail, + user_one, + user_two, + token_user_one + ): + res = app.get(url_token_detail, expect_errors=True) + assert res.status_code == 401 + + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_owner_can_delete( + self, + mock_method, + app, + user_one, + url_token_detail + ): + mock_method.return_value(True) + res = app.delete(url_token_detail, auth=user_one.auth) + assert res.status_code == 204 + + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_deleting_tokens_makes_api_view_inaccessible( + self, + mock_method, + app, + url_token_detail, + user_one + ): + mock_method.return_value(True) + app.delete(url_token_detail, auth=user_one.auth) + res = app.get(url_token_detail, auth=user_one.auth, expect_errors=True) + assert res.status_code == 404 + + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_updating_one_field_should_not_blank_others_on_patch_update( + self, + mock_revoke, + app, + token_user_one, + url_token_detail, + user_one, + read_scope + ): + mock_revoke.return_value = True + user_one_token = token_user_one + new_name = 'The token formerly known as Prince' + res = app.patch_json_api( + url_token_detail, + { + 'data': { + 'attributes': { + 'name': new_name, + }, + 'id': token_user_one._id, + 'type': 'tokens', + 'relationships': { + 'scopes': { + 'data': [ + { + 'type': 'scopes', + 'id': read_scope.name + } + ] + + } + } + } + }, + auth=user_one.auth + ) + user_one_token.reload() + assert res.status_code == 200 + + assert_dict_contains_subset( + {'name': new_name}, + res.json['data']['attributes'] + ) + assert res.json['data']['id'] == user_one_token._id + assert res.json['data']['relationships']['owner']['data']['id'] == user_one_token.owner._id + assert len(res.json['data']['embeds']['scopes']['data']) == 1 + assert res.json['data']['embeds']['scopes']['data'][0]['id'] == read_scope.name + + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_updating_an_instance_does_not_change_the_number_of_instances( + self, + mock_revoke, + app, + url_token_detail, + url_token_list, + token_user_one, + user_one, + read_scope + ): + mock_revoke.return_value = True + res = app.patch_json_api( + url_token_detail, + { + 'data': { + 'attributes': { + 'name': 'The token formerly known as Prince', + }, + 'relationships': { + 'scopes': { + 'data': [ + { + 'type': 'scopes', + 'id': read_scope.name + } + ] + } + }, + 'id': token_user_one._id, + 'type': 'tokens' + } + }, + auth=user_one.auth + ) + assert res.status_code == 200 + + res = app.get(url_token_list, auth=user_one.auth) + assert res.status_code == 200 + assert (len(res.json['data']) == 1) + + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_deleting_token_flags_instance_inactive( + self, + mock_method, + app, + url_token_detail, + user_one, + token_user_one + ): + mock_method.return_value(True) + app.delete(url_token_detail, auth=user_one.auth) + token_user_one.reload() + assert not token_user_one.is_active + + def test_read_does_not_return_token_id( + self, + app, + url_token_detail, + user_one + ): + res = app.get(url_token_detail, auth=user_one.auth) + assert res.status_code == 200 + assert 'token_id' not in res.json['data']['attributes'] + + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_update_token_does_not_return_token_id(self, mock_revoke, app, url_token_detail, user_one): + mock_revoke.return_value = True + res = app.put_json_api( + url_token_detail, + self.post_payload(), + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 200 + assert 'token_id' not in res.json['data']['attributes'] + assert 'token_id' not in res.json['data']['attributes'] + + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_update_token(self, mock_revoke, app, user_one, url_token_detail): + mock_revoke.return_value = True + res = app.put_json_api( + url_token_detail, + self.post_payload(), + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 200 + + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_update_token_add_scope(self, mock_revoke, app, user_one, token_user_one, url_token_detail): + mock_revoke.return_value = True + original_scope = token_user_one.scopes.first() + scope = ApiOAuth2ScopeFactory() + res = app.put_json_api( + url_token_detail, + self.post_payload(scopes=scope.name), + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 200 + scopes_data = res.json['data']['embeds']['scopes']['data'] + assert len(scopes_data) == 1 + assert scopes_data[0]['id'] == scope.name + assert scope.name != original_scope.name + + @pytest.mark.enable_implicit_clean + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_non_owner_cant_delete( + self, + mock_revoke, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): + mock_revoke.return_value = True + res = app.delete( + url_token_detail, + auth=user_two.auth, + expect_errors=True + ) + assert res.status_code == 403 + + @pytest.mark.enable_implicit_clean + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_create_with_nonexistant_scope_fails( + self, + mock_revoke, + url_token_list, + user_one, + app, + ): + mock_revoke.return_value = True + + injected_scope = self.post_payload( + name='A shiny invalid token', + scopes='osf.admin' + ) + res = app.post_json_api( + url_token_list, + injected_scope, + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 404 + + @pytest.mark.enable_implicit_clean + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_create_with_private_scope_fails( + self, + mock_revoke, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two, + private_scope + ): + assert not private_scope.is_public + nonsense_scope = self.post_payload( + scopes=private_scope.name + ) + res = app.post_json_api( + url_token_list, + nonsense_scope, + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 400 + + @pytest.mark.enable_implicit_clean + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_update_with_nonexistant_scope_fails( + self, + mock_revoke, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): + injected_scope = self.post_payload( + name='A shiny invalid token', + scopes='osf.admin' + ) + res = app.put_json_api( + url_token_detail, + injected_scope, + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 404 + + @pytest.mark.enable_implicit_clean + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_update_with_private_scope_fails( + self, + mock_revoke, + url_token_detail, + user_one, + app, + private_scope + ): + private_scope = self.post_payload( + name='A shiny invalid token', + scopes=private_scope.name + ) + res = app.put_json_api( + url_token_detail, + private_scope, + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 400 + + @pytest.mark.enable_implicit_clean + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_update_token_incorrect_type( + self, + mock_revoke, + app, + url_token_detail, + user_one, + ): + res = app.put_json_api( + url_token_detail, + self.post_payload(type_payload='Wrong type.'), + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 409 + + @pytest.mark.enable_implicit_clean + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_update_token_no_type( + self, + mock_revoke, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): + res = app.put_json_api( + url_token_detail, + self.post_payload(type_payload=''), + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 400 + + @pytest.mark.enable_implicit_clean + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_update_token_no_attributes( + self, + mock_revoke, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): + res = app.put_json_api( + url_token_detail, + { + 'id': token_user_one._id, + 'type': 'tokens', + 'name': 'The token formerly known as Prince' + }, + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 400 + + @pytest.mark.enable_implicit_clean + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_partial_update_token_incorrect_type( + self, + mock_revoke, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): + res = app.patch_json_api( + url_token_detail, + self.post_payload(type_payload='Wrong type.'), + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 409 + + @pytest.mark.enable_implicit_clean + @mock.patch('framework.auth.cas.CasClient.revoke_tokens') + def test_partial_update_token_no_type( + self, + mock_revoke, + app, + url_token_list, + url_token_detail, + token_user_one, + user_one, + user_two + ): + res = app.patch_json_api( + url_token_detail, + self.post_payload(type_payload=''), + auth=user_one.auth, + expect_errors=True + ) + assert res.status_code == 400 diff --git a/framework/auth/oauth_scopes.py b/framework/auth/oauth_scopes.py index 67644050383..796023228df 100644 --- a/framework/auth/oauth_scopes.py +++ b/framework/auth/oauth_scopes.py @@ -342,6 +342,7 @@ class ComposedScopes: + REVIEWS_WRITE\ + PREPRINT_ALL_WRITE\ + GROUP_WRITE\ + + TOKENS_WRITE\ + ( CoreScopes.CEDAR_METADATA_RECORD_WRITE, CoreScopes.WRITE_COLLECTION_SUBMISSION_ACTION, From f4d06ddca718adcbf8c8178108a2293d3b47d7d7 Mon Sep 17 00:00:00 2001 From: ihorsokhanexoft Date: Wed, 9 Apr 2025 16:55:45 +0300 Subject: [PATCH 2/9] [ENG-7749] admin user can publish preprint in admin app even if he isn't a contributor (#11084) ## Purpose `set_published` method checks if this action is performed by admin contributor, however admins in admin app should be able to perform this action without the permission ## Changes Handle this case with an additional parameter in method because if we add check like "if is_admin or is_admin_contributor" there may be a case when admin user opens preprint and publishes it via API that he shouldn't be able to do ## Ticket https://openscience.atlassian.net/browse/ENG-7749 --- admin/preprints/views.py | 12 +++++++++++- admin_tests/preprints/test_views.py | 29 +++++++++++++++++++++++++++++ osf/models/preprint.py | 4 ++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/admin/preprints/views.py b/admin/preprints/views.py index 14427df42f2..a936c27582e 100644 --- a/admin/preprints/views.py +++ b/admin/preprints/views.py @@ -18,6 +18,7 @@ from admin.preprints.forms import ChangeProviderForm, MachineStateForm from api.share.utils import update_share +from api.providers.workflows import Workflows from osf.exceptions import PreprintStateError @@ -43,6 +44,7 @@ ) from osf.utils.workflows import DefaultStates from website import search +from website.preprints.tasks import on_preprint_updated class PreprintMixin(PermissionRequiredMixin): @@ -558,7 +560,15 @@ class PreprintMakePublishedView(PreprintMixin, View): def post(self, request, *args, **kwargs): preprint = self.get_object() - preprint.set_published(True, request, True) + preprint.set_published( + published=True, + auth=request, + save=True, + ignore_permission=True + ) + if preprint.provider and preprint.provider.reviews_workflow == Workflows.POST_MODERATION.value: + on_preprint_updated.apply_async(kwargs={'preprint_id': preprint._id}) + return redirect(self.get_success_url()) class PreprintUnwithdrawView(PreprintMixin, View): diff --git a/admin_tests/preprints/test_views.py b/admin_tests/preprints/test_views.py index c2f8ac40e6a..1fb9d68482d 100644 --- a/admin_tests/preprints/test_views.py +++ b/admin_tests/preprints/test_views.py @@ -21,6 +21,7 @@ from osf.models.admin_log_entry import AdminLogEntry from osf.models.spam import SpamStatus from osf.utils.workflows import DefaultStates, RequestTypes +from osf.utils.permissions import ADMIN from admin_tests.utilities import setup_view, setup_log_view, handle_post_view_request @@ -768,3 +769,31 @@ def test_no_permission_raises_error(self, req, preprint): request.user = req.user with pytest.raises(PermissionDenied): views.PreprintMachineStateView.as_view()(request, guid=preprint._id) + + +@pytest.mark.urls('admin.base.urls') +class TestPreprintMakePublishedView: + + @pytest.fixture() + def plain_view(self): + return views.PreprintMakePublishedView + + def test_admin_user_can_publish_preprint(self, user, preprint, plain_view): + admin_group = Group.objects.get(name='osf_admin') + preprint.is_published = False + preprint.save() + + # user isn't admin contributor in the preprint + assert preprint.contributors.filter(id=user.id).exists() is False + assert preprint.has_permission(user, ADMIN) is False + + request = RequestFactory().post(reverse('preprints:make-published', kwargs={'guid': preprint._id})) + request.user = user + + admin_group.permissions.add(Permission.objects.get(codename='change_node')) + user.groups.add(admin_group) + + plain_view.as_view()(request, guid=preprint._id) + preprint.reload() + + assert preprint.is_published diff --git a/osf/models/preprint.py b/osf/models/preprint.py index ab30cbff39f..162ab8b00a8 100644 --- a/osf/models/preprint.py +++ b/osf/models/preprint.py @@ -785,8 +785,8 @@ def set_primary_file(self, preprint_file, auth, save=False): self.save() update_or_enqueue_on_preprint_updated(preprint_id=self._id, saved_fields=['primary_file']) - def set_published(self, published, auth, save=False): - if not self.has_permission(auth.user, ADMIN): + def set_published(self, published, auth, save=False, ignore_permission=False): + if not ignore_permission and not self.has_permission(auth.user, ADMIN): raise PermissionsError('Only admins can publish a preprint.') if self.is_published and not published: From 75c4d9dfee7c2226356a34bc4635e46a8655e965 Mon Sep 17 00:00:00 2001 From: ihorsokhanexoft Date: Wed, 9 Apr 2025 17:01:57 +0300 Subject: [PATCH 3/9] [ENG-7751] Fixed contributors reordering on registration metadata page (#11083) ## Purpose Contributors ordering in registration metadata is not updated ## Changes `index` field is read only if we inherit from `NodeContributorsSerializer`. The new one `NodeContributorDetailSerializer` overrides this field to be editable and inherits from `NodeContributorsSerializer` Also contributors ordering is different on registration metadata and metadata editing pages, thus it was fixed by adding the ordering attribute Also added tests ## Ticket https://openscience.atlassian.net/browse/ENG-7751 --- api/registrations/serializers.py | 4 +- api/registrations/views.py | 2 + .../test_registration_contributor_detail.py | 111 ++++++++++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 api_tests/registrations/views/test_registration_contributor_detail.py diff --git a/api/registrations/serializers.py b/api/registrations/serializers.py index 0e3542131fe..33ffb1bda18 100644 --- a/api/registrations/serializers.py +++ b/api/registrations/serializers.py @@ -20,7 +20,7 @@ NodeLicenseRelationshipField, NodeLinksSerializer, NodeLicenseSerializer, - NodeContributorsSerializer, + NodeContributorDetailSerializer, NodeContributorsCreateSerializer, RegistrationProviderRelationshipField, get_license_details, @@ -908,7 +908,7 @@ def get_absolute_url(self, obj): ) -class RegistrationContributorsSerializer(NodeContributorsSerializer): +class RegistrationContributorsSerializer(NodeContributorDetailSerializer): def get_absolute_url(self, obj): return absolute_reverse( 'registrations:registration-contributor-detail', diff --git a/api/registrations/views.py b/api/registrations/views.py index 29305d23ce2..8254ea69edf 100644 --- a/api/registrations/views.py +++ b/api/registrations/views.py @@ -280,6 +280,8 @@ class RegistrationContributorsList(BaseContributorList, mixins.CreateModelMixin, required_read_scopes = [CoreScopes.NODE_REGISTRATIONS_READ] required_write_scopes = [CoreScopes.NODE_REGISTRATIONS_WRITE] + ordering = ('_order',) + permission_classes = ( ContributorDetailPermissions, drf_permissions.IsAuthenticatedOrReadOnly, diff --git a/api_tests/registrations/views/test_registration_contributor_detail.py b/api_tests/registrations/views/test_registration_contributor_detail.py new file mode 100644 index 00000000000..37ff567ab8d --- /dev/null +++ b/api_tests/registrations/views/test_registration_contributor_detail.py @@ -0,0 +1,111 @@ +from tests.base import ApiTestCase + +from osf_tests.factories import ( + ProjectFactory, + RegistrationFactory, + AuthUserFactory, +) +from api.base.settings.defaults import API_BASE + + +class TestRegistrationContributorDetailTestCase(ApiTestCase): + + def setUp(self): + super().setUp() + self.creator = AuthUserFactory() + self.read_contributor = AuthUserFactory() + self.write_contributor = AuthUserFactory() + self.public_project = ProjectFactory( + title='Public Project', + is_public=True, + creator=self.creator + ) + self.public_registration = RegistrationFactory(project=self.public_project, creator=self.creator) + + self.creator_id = f'{self.public_registration._id}-{self.creator._id}' + self.read_contributor_id = f'{self.public_registration._id}-{self.read_contributor._id}' + self.write_contributor_id = f'{self.public_registration._id}-{self.write_contributor._id}' + + self.public_url = f'/{API_BASE}registrations/{self.public_registration._id}/' + self.add_contributor_url = f'/{API_BASE}registrations/{self.public_registration._id}/contributors/' + self.edit_contributor_url = f'/{API_BASE}registrations/{self.public_registration._id}/contributors/' + + def form_contributors_create_payload(self, permission, contributor): + return { + 'data': { + 'type': 'contributors', + 'attributes': { + 'permission': permission, + 'bibliographic': True + }, + 'relationships': { + 'users': { + 'data': { + 'type': 'users', + 'id': contributor._id + } + } + } + } + } + + def form_contributor_edit_payload(self, contributor, index): + return { + 'data': { + 'id': f'{self.public_registration._id}-{contributor._id}', + 'attributes': { + 'index': index + }, + 'relationships': {}, + 'type': 'contributors' + } + } + + def test_initial_contributors_ordering_is_correct(self): + read_payload = self.form_contributors_create_payload('read', self.read_contributor) + write_payload = self.form_contributors_create_payload('write', self.write_contributor) + self.app.post_json_api( + self.add_contributor_url, + read_payload, + auth=self.creator.auth + ) + self.app.post_json_api( + self.add_contributor_url, + write_payload, + auth=self.creator.auth + ) + + contributors = self.app.get(self.add_contributor_url, auth=self.creator.auth) + # shift by 1 because creator is the first contributor + assert contributors.json['data'][1]['id'] == self.read_contributor_id + assert contributors.json['data'][2]['id'] == self.write_contributor_id + + def test_contributors_reordering_works_correctly(self): + read_payload = self.form_contributors_create_payload('read', self.read_contributor) + write_payload = self.form_contributors_create_payload('write', self.write_contributor) + self.app.post_json_api( + self.add_contributor_url, + read_payload, + auth=self.creator.auth + ) + self.app.post_json_api( + self.add_contributor_url, + write_payload, + auth=self.creator.auth + ) + + self.app.patch_json_api( + self.edit_contributor_url + f'{self.creator._id}/', + self.form_contributor_edit_payload(self.creator, 2), + auth=self.creator.auth + ) + self.app.patch_json_api( + self.edit_contributor_url + f'{self.read_contributor._id}/', + self.form_contributor_edit_payload(self.read_contributor, 0), + auth=self.creator.auth + ) + + contributors = self.app.get(self.add_contributor_url, auth=self.creator.auth) + assert contributors.json['data'][0]['id'] == self.read_contributor_id + assert contributors.json['data'][1]['id'] == self.write_contributor_id + assert contributors.json['data'][2]['id'] == self.creator_id From 9fb209e57bf766272abb5de20ad905f1d717001c Mon Sep 17 00:00:00 2001 From: mkovalua Date: Wed, 9 Apr 2025 17:14:49 +0300 Subject: [PATCH 4/9] [ENG-5751] remove old keen logic (#11082) * ENG-5751 remove old keen logic * add migration * remove legacy keen from metrics .js (maybe another keen logic is also needed to be removed in the files) * remove dashboard keen (it is not used in any views) * ENG-5751 code updates * ENG-5751 remove keen dependency from poetry * ENG-5751 code updates * code updates * ENG-5751 resolve PR comments * ENG-5751 add default '' analytics_key * ENG-5751 remove base_analytics.html * add keen-dashboards.css * fix/ENG-5751 it seems 'keen' is used in base mako files and keen js rely on it and it is not needed to be deleted * fix/ENG-5751 resolve PR comments (not confident about 'getUniqueId' implementation https://stackoverflow.com/questions/18230217/javascript-generate-a-random-number-within-a-range-using-crypto-getrandomvalues ) * remove keen-tracking * update getUniqueId * fix/ENG-5751 PR comments related updates * fix/ENG-5751 PR comments related updates * remove geoip from deps and location code from app * PR comment related updates https://github.com/CenterForOpenScience/osf.io/pull/10992#discussion_r2021990050 * fix migrations conflicts * a few tidy-ups: * Relocate code changes in api.nodes.serializers to be more compact when squished. * Really delete `cumulative_plos_metrics.py`. Per Eric's comments on Slack, we no longer need this. * Remove `cumulative_plos_metrics.py`-related config from website.settings.defaults.py * Add attributive comments for borrowed code in metrics library. --------- Co-authored-by: Fitz Elliott --- admin/base/settings/defaults.py | 16 - admin/metrics/views.py | 2 - admin/nodes/views.py | 1 - admin/static/css/keen-dashboards.css | 6 +- admin/static/js/metrics/metrics.es6.js | 145 ----- admin/static/js/pages/metrics-page.js | 8 +- admin/static/js/sales_analytics/dashboard.js | 409 ------------ admin/templates/base_analytics.html | 38 -- admin/templates/metrics/osf_metrics.html | 9 - .../templates/sales_analytics/dashboard.html | 223 ------- admin/webpack.admin.config.js | 1 - api/nodes/serializers.py | 3 +- .../nodes/serializers/test_serializers.py | 1 - .../commands/cumulative_plos_metrics.py | 162 ----- ...029_remove_abstractnode_keenio_read_key.py | 17 + osf/models/node.py | 18 +- osf_tests/test_node.py | 2 - package.json | 2 - poetry.lock | 581 +----------------- pyproject.toml | 3 - ...late_popular_projects_and_registrations.py | 75 --- ...late_popular_projects_and_registrations.py | 95 --- website/project/utils.py | 73 --- website/routes.py | 21 - website/settings/defaults.py | 32 - website/settings/local-ci.py | 15 - website/static/js/keen.js | 275 --------- website/static/js/metrics.js | 106 ++++ website/static/js/pages/base-page.js | 6 +- website/templates/base.mako | 19 - yarn.lock | 43 +- 31 files changed, 133 insertions(+), 2274 deletions(-) mode change 100755 => 100644 admin/static/css/keen-dashboards.css delete mode 100644 admin/static/js/sales_analytics/dashboard.js delete mode 100644 admin/templates/base_analytics.html delete mode 100644 admin/templates/sales_analytics/dashboard.html delete mode 100644 osf/management/commands/cumulative_plos_metrics.py create mode 100644 osf/migrations/0029_remove_abstractnode_keenio_read_key.py delete mode 100644 scripts/populate_popular_projects_and_registrations.py delete mode 100644 scripts/tests/test_populate_popular_projects_and_registrations.py delete mode 100644 website/static/js/keen.js create mode 100644 website/static/js/metrics.js diff --git a/admin/base/settings/defaults.py b/admin/base/settings/defaults.py index f0647492f67..579b920949e 100644 --- a/admin/base/settings/defaults.py +++ b/admin/base/settings/defaults.py @@ -199,22 +199,6 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' NOSE_ARGS = ['--verbosity=2'] -# Keen.io settings in local.py -KEEN_PROJECT_ID = osf_settings.KEEN['private']['project_id'] -KEEN_READ_KEY = osf_settings.KEEN['private']['read_key'] -KEEN_WRITE_KEY = osf_settings.KEEN['private']['write_key'] - -KEEN_CREDENTIALS = { - 'keen_ready': False -} - -if KEEN_CREDENTIALS['keen_ready']: - KEEN_CREDENTIALS.update({ - 'keen_project_id': KEEN_PROJECT_ID, - 'keen_read_key': KEEN_READ_KEY, - 'keen_write_key': KEEN_WRITE_KEY - }) - # Set in local.py DESK_KEY = '' diff --git a/admin/metrics/views.py b/admin/metrics/views.py index 3628f8278e4..4255181c7ff 100644 --- a/admin/metrics/views.py +++ b/admin/metrics/views.py @@ -2,7 +2,6 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from admin.base import settings -from admin.base.settings import KEEN_CREDENTIALS class MetricsView(PermissionRequiredMixin, TemplateView): @@ -11,7 +10,6 @@ class MetricsView(PermissionRequiredMixin, TemplateView): raise_exception = True def get_context_data(self, **kwargs): - kwargs.update(KEEN_CREDENTIALS.copy()) api_report_url = f'{settings.API_DOMAIN}_/metrics/reports/' kwargs.update({'metrics_url': api_report_url}) return super().get_context_data(**kwargs) diff --git a/admin/nodes/views.py b/admin/nodes/views.py index 2318778e369..fc16a3b0d05 100644 --- a/admin/nodes/views.py +++ b/admin/nodes/views.py @@ -643,7 +643,6 @@ def post(self, request, *args, **kwargs): node = self.get_object() node.is_public = False - node.keenio_read_key = '' # After set permissions callback for addon in node.get_addons(): diff --git a/admin/static/css/keen-dashboards.css b/admin/static/css/keen-dashboards.css old mode 100755 new mode 100644 index b8e7a507681..2b6b8a01b32 --- a/admin/static/css/keen-dashboards.css +++ b/admin/static/css/keen-dashboards.css @@ -113,8 +113,4 @@ body.application .return-link:hover, .hr-menu { background-color: lightgray; height: 1px; -} - -#reload-node-logs { - margin-left: 20px; -} +} \ No newline at end of file diff --git a/admin/static/js/metrics/metrics.es6.js b/admin/static/js/metrics/metrics.es6.js index 42da8ffd8ac..10d6281646a 100644 --- a/admin/static/js/metrics/metrics.es6.js +++ b/admin/static/js/metrics/metrics.es6.js @@ -5,20 +5,9 @@ require('keen-dataviz/dist/keen-dataviz.min.css'); var c3 = require('c3/c3.js'); -var keen = require('keen-js'); var keenDataviz = require('keen-dataviz'); -var keenAnalysis = require('keen-analysis'); var $ = require('jquery'); -var client = new keenAnalysis({ - projectId: keenProjectId, - readKey: keenReadKey -}); - -var publicClient = new keenAnalysis({ - projectId: keenPublicProjectId, - readKey: keenPublicReadKey -}); // Heights and Colors for the below rendered // Keen and c3 Data visualizations @@ -423,67 +412,6 @@ var getMetricTitle = function(metric, type) { return title; }; -var renderKeenMetric = function(element, type, query, height, colors, keenClient) { - if (!keenClient) { - keenClient = client; - } - - var chart = new keenDataviz() - .el(element) - .height(height) - .title(' ') - .type(type) - .prepare(); - - if (colors) { - chart.colors([colors]); - } - - keenClient - .run(query) - .then(function(res){ - var metricChart = chart.data(res); - metricChart.dataset.sortRows("desc", function(row) { - return row[1] - }); - metricChart.render(); - }) - .catch(function(err){ - chart.message(err.message); - }); -}; - -var renderNodeLogsForOneUserChart = function(user_id) { - var chart = new keenDataviz() - .el('#yesterdays-node-logs-by-user') - .height(bigMetricHeight) - .title('Individual Logs for ' + '' + user_id + '') - .type('line') - .prepare(); - - client - .query('count', { - event_collection: "node_log_events", - interval: "hourly", - group_by: "action", - filters: [{ - property_name: 'user_id', - operator: 'eq', - property_value: user_id - }], - timeframe: "previous_1_days", - timezone: "UTC" - }) - .then(function(res){ - chart - .data(res) - .render(); - }) - .catch(function(err){ - chart.message(err.message); - }); -}; - // called from pageview collection var differenceGrowthBetweenMetrics = function(query1, query2, totalQuery, element, colors) { @@ -837,46 +765,6 @@ var renderEmailDomainsChart = function() { }); }; -var NodeLogsPerUser = function() { - var chart = new keenDataviz() - .el('#yesterdays-node-logs-by-user') - .title(' ') - .height(bigMetricHeight) - .chartOptions({ - data: { - onclick: function (d, element) { - renderNodeLogsForOneUserChart(d.name); - } - } - }) - - .type('line') - .prepare(); - - client - .query('count', { - event_collection: 'node_log_events', - group_by: "user_id", - timeframe: 'previous_1_days', - interval: 'hourly' - }) - .then(function (res) { - var chartWithData = chart.data(res); - chartWithData.dataset.filterColumns(function (column, index) { - var logThreshhold = 25; - for (var i = 0; i < column.length; i++) { - if (column[i] > logThreshhold && column[0] != 'null' && column[0] != 'uj57r') { - return column; - } - } - }); - - chartWithData.render(); - }) - .catch(function (err) { - chart.message(err.message); - }); -}; var renderRawNodeMetrics = function() { var propertiesAndElements = [ @@ -966,7 +854,6 @@ var UserGainMetrics = function() { renderMainCounts(); renderWeeklyUserGainMetrics(); renderEmailDomainsChart(); - NodeLogsPerUser(); }; @@ -1084,36 +971,6 @@ function renderPreprintMetrics(timeframe) { // ><+><+><+<+><+ var DownloadMetrics = function() { - - // ********** legacy keen ********** - var totalDownloadsQuery = new keenAnalysis.Query("count", { - eventCollection: "file_stats", - timeframe: 'previous_1_days', - filters: [{ - property_name: 'action.type', - operator: 'eq', - property_value: 'download_file', - timezone: "UTC" - }] - }); - renderKeenMetric("#number-of-downloads", "metric", totalDownloadsQuery, - defaultHeight, defaultColor, publicClient); - - // ********** legacy keen ********** - var uniqueDownloadsQuery = new keenAnalysis.Query("count_unique", { - eventCollection: "file_stats", - timeframe: 'previous_1_days', - target_property: 'file.resource', - filters: [{ - property_name: 'action.type', - operator: 'eq', - property_value: 'download_file', - timezone: "UTC" - }] - }); - renderKeenMetric("#number-of-unique-downloads", "metric", uniqueDownloadsQuery, - defaultHeight, defaultColor, publicClient); - renderDownloadMetrics(); }; @@ -1138,13 +995,11 @@ function renderDownloadMetrics(timeframe) { module.exports = { UserGainMetrics: UserGainMetrics, - NodeLogsPerUser: NodeLogsPerUser, InstitutionMetrics: InstitutionMetrics, RawNumberMetrics: RawNumberMetrics, AddonMetrics: AddonMetrics, PreprintMetrics: PreprintMetrics, DownloadMetrics: DownloadMetrics, - KeenRenderMetrics: renderKeenMetric, RenderPreprintMetrics: renderPreprintMetrics, RenderDownloadMetrics: renderDownloadMetrics, }; diff --git a/admin/static/js/pages/metrics-page.js b/admin/static/js/pages/metrics-page.js index dedd46e0570..50b8d3c1049 100644 --- a/admin/static/js/pages/metrics-page.js +++ b/admin/static/js/pages/metrics-page.js @@ -1,19 +1,13 @@ 'use strict'; var $ = require('jquery'); -var keenAnalysis = require('keen-analysis'); var Metrics = require('js/metrics/metrics'); -keenAnalysis.ready(function() { - +$( document ).ready(function() { Metrics.UserGainMetrics(); - $('#reload-node-logs')[0].onclick = function() { - Metrics.NodeLogsPerUser(); - }; - $('#institution-tab')[0].onclick = function() { Metrics.InstitutionMetrics(); }; diff --git a/admin/static/js/sales_analytics/dashboard.js b/admin/static/js/sales_analytics/dashboard.js deleted file mode 100644 index 19eb217c27d..00000000000 --- a/admin/static/js/sales_analytics/dashboard.js +++ /dev/null @@ -1,409 +0,0 @@ -require('bootstrap'); -require('jquery'); -require('c3/c3.css'); - -var c3 = require('c3/c3.js'); -var keen = require('keen-js'); -var ss = require('simple-statistics'); - -var SalesAnalytics = function() { - // Metrics and visualization for the analytics dashboard. - var self = this; - - self.keenClient = new keen({ - projectId: keenProjectId, - readKey : keenReadKey - }); - - self.keenFilters = { - nullUserFilter: { - property_name: 'user.id', - operator: 'ne', - property_value: null - }, - inactiveUserFilter: { - property_name: 'user.id', - operator: 'ne', - property_value: '' - } - }; - self.getAverageUserSessionLength = function () { - var query = new keen.Query('select_unique', { - event_collection: 'pageviews', - timeframe: 'previous_7_days', - target_property: 'keen.timestamp', - group_by: ['user.id', 'sessionId'], - filters: [self.keenFilters.nullUserFilter] - }); - - var chart = self.prepareChart('keen-chart-average-session-user'); - - self.keenClient.run(query, function(error, response) { - if (error) { - document.getElementById('keen-chart-average-session-user').innerHTML = 'Keen Query Failure'; - console.log(error); - } - else { - var dataSet = self.extractDataSet(response); - var result = ss.mean(dataSet); - self.drawChart(chart, 'metric', 'Minutes', result/1000/60); - } - }); - }; - - self.getAverageMAUSessionLength = function() { - var query = new keen.Query('select_unique', { - event_collection: 'pageviews', - timeframe: 'previous_7_days', - target_property: 'keen.timestamp', - group_by: ['user.id', 'sessionId'], - filters: [self.keenFilters.nullUserFilter, self.keenFilters.inactiveUserFilter] - }); - - var chart = self.prepareChart('keen-chart-average-session-mau'); - - self.keenClient.run(query, function(error, response) { - if (error) { - document.getElementById('keen-chart-average-session-mau').innerHTML = 'Keen Query Failure.'; - console.log(error); - } - else { - var dataSet = self.extractDataSet(response); - var result = ss.mean(dataSet); - self.drawChart(chart, 'metric', 'Minutes', result/1000/60); - } - }); - }; - - self.getAverageUserSessionHistory = function(init, numberOfWeeks, weekEnd, weekStart, weeklyResult) { - if (init === true) { - var date = new Date(); - date.setHours(0, 0, 0, 0); - date.setDate(date.getDate() - date.getDay()); - weekEnd = new Date(date); - weekStart = new Date(date.setDate(date.getDate() - 7)); - weeklyResult = []; - self.getAverageUserSessionHistory(false, numberOfWeeks, weekEnd, weekStart, weeklyResult); - return; - } - - if (numberOfWeeks === 0) { - var chart = self.prepareChart('keen-chart-average-session-history'); - chart.attributes({title: 'Average User/MAU Session Length History of Past 12 Weeks', width: '100%', height: 450}); - chart.adapter({chartType: 'columnchart'}); - chart.chartOptions({ - hAxis: {title: 'Week'}, - vAxis: {title: 'Minutes'} - }); - chart.parseRawData({result: weeklyResult}).render(); - return; - } - - document.getElementById('keen-chart-average-session-history').innerHTML = 'loading ... : ' + (12 - numberOfWeeks) + ' / 12'; - var queryUser = new keen.Query('select_unique', { - event_collection: 'pageviews', - timeframe: { - start: weekStart.toISOString(), - end: weekEnd.toISOString() - }, - target_property: 'keen.timestamp', - group_by: ['user.id', 'sessionId'], - filters: [self.keenFilters.nullUserFilter] - }); - var queryMAU = new keen.Query('select_unique', { - event_collection: 'pageviews', - timeframe: { - start: weekStart.toISOString(), - end: weekEnd.toISOString() - }, - target_property: 'keen.timestamp', - group_by: ['user.id', 'sessionId'], - filters: [self.keenFilters.nullUserFilter, self.keenFilters.inactiveUserFilter] - }); - - self.keenClient.run([queryUser, queryMAU], function (error, response) { - var resultUser; - var resultMAU; - if (error) { - document.getElementById('keen-chart-average-session-history').innerHTML = 'failure ... : ' + (12 - numberOfWeeks) + ' / 12'; - console.log(error); - resultUser = resultMAU = -1; - } - else { - var dataSetUser = self.extractDataSet(response[0]); - var dataSetMAU = self.extractDataSet(response[1]); - resultUser = ss.mean(dataSetUser); - resultMAU = ss.mean(dataSetMAU); - } - var item = { - timeframe: { - start: weekStart.toISOString(), - end: weekEnd.toISOString() - }, - value: [ - { category: 'User', result: resultUser/1000/60 }, - { category: 'MAU', result: resultMAU/1000/60 } - ] - }; - weeklyResult.push(item); - - weekEnd = new Date(weekStart); - weekStart.setDate(weekEnd.getDate() - 7); - numberOfWeeks--; - self.getAverageUserSessionHistory(false, numberOfWeeks, weekEnd, weekStart, weeklyResult); - }); - }; - - self.getOSFProductUsage = function() { - var query = new Keen.Query('select_unique', { - event_collection: 'pageviews', - timeframe: 'previous_1_months', - target_property: 'parsedPageUrl.path', - group_by: ['user.id', 'parsedPageUrl.domain'], - filters: [self.keenFilters.nullUserFilter, self.keenFilters.inactiveUserFilter] - }); - - var chartMoreThanTwo = self.prepareChart('keen-chart-osf-product-usage-mt2'); - var chartMeetings = self.prepareChart('keen-chart-osf-product-usage-mee'); - var chartInstitutions = self.prepareChart('keen-chart-osf-product-usage-ins'); - - var request = self.keenClient.run(query, function(error, response) { - if (error) { - document.getElementById('keen-chart-osf-product-usage-mt2').innerHTML = 'Keen Query Failure'; - } - else { - var userProductMap = self.numberOfUsers(response); - self.drawChart(chartMoreThanTwo, 'piechart', '', [ - {products: 'OSF Only', count: userProductMap.osf.length - userProductMap.moreThanTwo.length}, - {products: '2+ Products', count: userProductMap.moreThanTwo.length} - ]); - self.drawChart(chartMeetings, 'piechart', '', [ - {products: 'No Meetings', count: userProductMap.osf.length - userProductMap.meetings.length}, - {products: 'Meetings', count: userProductMap.meetings.length} - ]); - self.drawChart(chartInstitutions, 'piechart', '', [ - {products: 'No Institutions', count: userProductMap.osf.length - userProductMap.institutions.length}, - {products: 'Institutions', count: userProductMap.institutions.length} - ]); - } - }); - }; - self.getUserCount = function(userCount) { - // User count for each peroduct (osf, osf4m, institution) - var chart = c3.generate({ - bindto: '#db-chart-user-count', - data: { - columns: [['Count'].concat(userCount.count)], - type: 'bar', - colors: { - Count: '#ff5a5f', - } - }, - axis: { - x: { - type: 'category', - categories: userCount.tags, - }, - rotated: true - }, - bar: { - width: { - ratio: 0.5 // this makes bar width 50% of length between ticks - } - } - }); - }; - - self.getUserPercentage = function(userCount) { - var chart = c3.generate({ - bindto: '#db-chart-user-percent', - data: { - columns: [['Percentage'].concat(userCount.percent)], - type: 'bar', - colors: { - Count: '#ff8083', - } - }, - axis: { - x: { - type: 'category', - categories: userCount.tags, - }, - rotated: true - }, - bar: { - width: { - ratio: 0.5 // this makes bar width 50% of length between ticks - } - } - }); - }; - - self.getMultiProductCountYearly = function(multiProductMetricsYearly) { - // Number of users that use 2+ products - var chart = self.drawMetric('db-chart-multi-product-yearly', - multiProductMetricsYearly.multi_product_count, - '#e57fc2', - 'Users'); - }; - - self.getMultiProductCountMonthly = function(multiProductMetricsMonthly) { - var chart = self.drawMetric('db-chart-multi-product-monthly', - multiProductMetricsMonthly.multi_product_count, - '#e57fc2', - 'Users'); - }; - - self.getCrossProductCountYearly = function(multiProductMetricsYearly) { - // Number of users that use a product different from their entry points. - var chart = self.drawMetric('db-chart-cross-product-yearly', - multiProductMetricsYearly.cross_product_count, - '#6ab975', - 'Users'); - }; - - self.getCrossProductCountMonthly = function(multiProductMetricsMonthly) { - var chart = self.drawMetric('db-chart-cross-product-monthly', - multiProductMetricsMonthly.cross_product_count, - '#6ab975', - 'Users'); - }; - - self.getMultiActionCountYearly = function(multiProductMetricsYearly) { - // Number of users that have more than one type of action. - var chart = self.drawMetric('db-chart-multi-action-yearly', - multiProductMetricsYearly.multi_action_count, - '#4C72B7', - 'Users'); - }; - - self.getMultiActionCountMonthly = function(multiProductMetricsMonthly) { - var chart = self.drawMetric('db-chart-multi-action-monthly', - multiProductMetricsMonthly.multi_action_count, - '#4C72B7', - 'Users'); - }; - - self.getRepeatActionCountMonthly = function(repeatActionUserMonthly) { - var chart = self.drawMetric('db-chart-repeat-action-monthly', - repeatActionUserMonthly.repeat_action_count, - '#b75c4c', - 'Users'); - }; - - self.getTotalUserCount = function(userCount) { - var chart = self.drawMetric('db-chart-total-user', - userCount.total, - '#005c66', - 'Users'); - }; - - self.prepareChart = function(elementId) { - var chart = new keen.Dataviz(); - return chart.el(document.getElementById(elementId)).prepare(); - }; - - self.drawChart = function(chart, type, title, result, color) { - chart.attributes({title: title, width: '100%'}); - chart.adapter({chartType: type}); - chart.parseRawData({result: result}); - chart.render(); - }; - - self.drawMetric = function(elementId, result, color, title) { - var chart = new keen.Dataviz() - .el(document.getElementById(elementId)) - .parseRawData({ result: result }) - .chartType('metric') - .colors([color]) - .title(title) - .render(); - }; - - self.init = function() { - console.log('init'); - keen.ready(self.run()); - }; - - self.run = function() { - console.log('run'); - self.getOSFProductUsage(); - self.getAverageUserSessionLength(); - self.getAverageMAUSessionLength(); - self.getAverageUserSessionHistory(true, 12, null, null, null); - self.getUserCount(userCount); - self.getUserPercentage(userCount); - self.getMultiProductCountYearly(multiProductMetricsYearly); - self.getMultiProductCountMonthly(multiProductMetricsMonthly); - self.getCrossProductCountYearly(multiProductMetricsYearly); - self.getCrossProductCountMonthly(multiProductMetricsMonthly); - self.getMultiActionCountYearly(multiProductMetricsYearly); - self.getMultiActionCountMonthly(multiProductMetricsMonthly); - self.getRepeatActionCountMonthly(repeatActionUserMonthly); - self.getTotalUserCount(userCount); - }; - - self.extractDataSet = function(keenResult) { - if (!keenResult) { - return 0; - } - - var beginTime; - var endTime; - var deltaSet = []; - - for (var i in keenResult.result) { - var session = keenResult.result[i]; - if (session.hasOwnProperty('result')) { - if (session.result.length === 1) { - // TODO: take care of the situation where there is only one 'keen.timestamp' - continue; - } - beginTime = Date.parse(session.result[0]); - endTime = Date.parse(session.result[session.result.length-1]); - deltaSet.push(endTime - beginTime); - } - } - return deltaSet; - }; - - self.numberOfUsers = function(keenResult, filters) { - var userProductMap = { - 'osf': [], - 'meetings': [], - 'institutions': [], - 'moreThanTwo': [] - }; - for (var i in keenResult.result) { - var session = keenResult.result[i]; - if (session.hasOwnProperty('result') && session.hasOwnProperty('user.id')) { - if (session.hasOwnProperty('parsedPageUrl.domain') && session['parsedPageUrl.domain'] === 'staging.osf.io') { - userProductMap.osf.push(session['user.id']); - var paths = session.result; - var numberOfProducts = 0; - var meetings, institutions; - meetings = institutions = false; - for (var j in paths) { - if (meetings === false && paths[j].startsWith('/meetings/')) { - userProductMap.meetings.push(session['user.id']); - meetings = true; - numberOfProducts ++; - } - else if (institutions === false && paths[j].startsWith('/institutions/')) { - userProductMap.institutions.push(session['user.id']); - meetings = true; - numberOfProducts ++; - } - } - if (numberOfProducts > 0) { - userProductMap.moreThanTwo.push(session['user.id']); - } - } - } - } - return userProductMap; - }; -}; - -var salesAnalytics = new SalesAnalytics(); -salesAnalytics.init(); diff --git a/admin/templates/base_analytics.html b/admin/templates/base_analytics.html deleted file mode 100644 index cc89be8c59e..00000000000 --- a/admin/templates/base_analytics.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block top_includes %} - - - {% block top_analytics %} - {% endblock top_analytics %} - -{% endblock top_includes %} - -{% block title %} - OSF Sales Analytics -{% endblock title %} - - -{% block content %} - - {% block topmenu %} - - Analytics Dashboard - - {% endblock topmenu %} - - {% block title_analytics %} - - {% endblock title_analytics %} - - {% block content_analytics %} - - {% endblock content_analytics %} - -{% endblock content %} diff --git a/admin/templates/metrics/osf_metrics.html b/admin/templates/metrics/osf_metrics.html index f01f0513a7e..3046d7f8103 100644 --- a/admin/templates/metrics/osf_metrics.html +++ b/admin/templates/metrics/osf_metrics.html @@ -10,12 +10,6 @@ @@ -196,9 +190,6 @@

User Data

Yesterday's Node logs by user -
-
-
The last day of node logs by user, if a user had over 100 node logs.
diff --git a/admin/templates/sales_analytics/dashboard.html b/admin/templates/sales_analytics/dashboard.html deleted file mode 100644 index f3218b47a15..00000000000 --- a/admin/templates/sales_analytics/dashboard.html +++ /dev/null @@ -1,223 +0,0 @@ -{% extends 'base_analytics.html' %} -{% load static %} -{% load filters %} - -{% block top_analytics %} - -{% endblock top_analytics %} - - -{% block title_analytics %} -

Dashboard

-{% endblock title_analytics %} - -{% block content_analytics %} - - - Dashboard - - - - - -
- -
-
-
-
-
-
- Average User/MAU Session Length History of past 12 weeks -
-
-
-
...
-
-
-
- Notes about this chart -
-
-
-
-
-
-
-
- User Count by Product -
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- User count/percentage for each peroduct (osf, osf4m, institution) -
-
-
-
-
-
-
-
-
-
- OSF Products Users View in the Past Month -
-
-
...
-
...
-
-
- Notes about this chart -
-
-
-
-
-
- OSF Products Users View in the Past Month -
-
-
...
-
...
-
-
- Notes about this chart -
-
-
- - -
-
-
- Average User Session Length of Previous 7 days -
-
-
-
-
-
-
- Average MAU Session Length of Previous 7 days -
-
-
...
-
-
-
-
-
-
- Multi-product User Count (Year) -
-
-
-
-
-
-
- Multi-product User Count (Month) -
-
-
-
-
- Number of users that use 2+ products -
-
-
-
-
-
- Cross-product User Count (Year) -
-
-
-
-
-
-
- Cross-product User Count (Month) -
-
-
-
-
- Number of users that use a product other than their entry points. -
-
-
-
-
-
- Multi-action User Count (Year) -
-
-
-
-
-
-
- Multi-action User Count (Month) -
-
-
-
-
- Number of users that have more than one type of actions. -
-
-
-
-
-
- Repetitive actions User Count (Month) -
-
-
-
-
-
-
- Total User Count -
-
-
-
-
- Total number of Users -
-
-
-
-
-
- - -{% endblock content_analytics %} - -{% block bottom_js %} - -{% endblock %} diff --git a/admin/webpack.admin.config.js b/admin/webpack.admin.config.js index e9987f2ed9d..be4bdb6f53d 100644 --- a/admin/webpack.admin.config.js +++ b/admin/webpack.admin.config.js @@ -32,7 +32,6 @@ var config = Object.assign({}, common, { entry: { 'admin-base-page': staticAdminPath('js/pages/base-page.js'), 'admin-registration-edit-page': staticAdminPath('js/pages/admin-registration-edit-page.js'), - 'dashboard': staticAdminPath('js/sales_analytics/dashboard.js'), 'metrics-page': staticAdminPath('js/pages/metrics-page.js'), 'banners': staticAdminPath('js/banners/banners.js'), 'brands': staticAdminPath('js/brands/brands.js'), diff --git a/api/nodes/serializers.py b/api/nodes/serializers.py index 879eff63d49..473b439ffd5 100644 --- a/api/nodes/serializers.py +++ b/api/nodes/serializers.py @@ -274,7 +274,8 @@ class NodeSerializer(TaxonomizableSerializerMixin, JSONAPISerializer): tags = ValuesListField(attr_name='name', child=ser.CharField(), required=False) access_requests_enabled = ShowIfVersion(ser.BooleanField(read_only=False, required=False), min_version='2.0', max_version='2.8') node_license = NodeLicenseSerializer(required=False, source='license') - analytics_key = ShowIfAdminScopeOrAnonymous(ser.CharField(read_only=True, source='keenio_read_key')) + # 'analytics_key' can be removed after another version release. + analytics_key = ShowIfAdminScopeOrAnonymous(ser.CharField(read_only=True, default='')) template_from = ser.CharField( required=False, allow_blank=False, allow_null=False, help_text='Specify a node id for a node you would like to use as a template for the ' diff --git a/api_tests/nodes/serializers/test_serializers.py b/api_tests/nodes/serializers/test_serializers.py index 1d6f7a14da7..d139652796b 100644 --- a/api_tests/nodes/serializers/test_serializers.py +++ b/api_tests/nodes/serializers/test_serializers.py @@ -48,7 +48,6 @@ def test_node_serializer(self, user): assert attributes['registration'] == node.is_registration assert attributes['fork'] == node.is_fork assert attributes['collection'] == node.is_collection - assert attributes['analytics_key'] == node.keenio_read_key assert attributes['wiki_enabled'] == node.has_addon('wiki') # Relationships diff --git a/osf/management/commands/cumulative_plos_metrics.py b/osf/management/commands/cumulative_plos_metrics.py deleted file mode 100644 index 27a6aa8a47e..00000000000 --- a/osf/management/commands/cumulative_plos_metrics.py +++ /dev/null @@ -1,162 +0,0 @@ -import csv -import io -from datetime import datetime -import logging -import requests -import tempfile - -from django.core.management.base import BaseCommand -from keen import KeenClient -from requests_oauthlib import OAuth2 - -from addons.osfstorage.models import OsfStorageFile -from framework.celery_tasks import app as celery_app -from osf.metrics import PreprintView, PreprintDownload -from osf.models import Guid, Node, OSFUser, Preprint, \ - Registration, TrashedFile -from osf.utils.fields import ensure_str -from website.settings import ( - KEEN, - PLOS_METRICS_BASE_FOLDER, - PLOS_METRICS_INITIAL_FILE_DOWNLOAD_URL, - PLOS_METRICS_OSF_TOKEN, -) - - -DEFAULT_API_VERSION = '2.20' -TEMP_FOLDER = tempfile.mkdtemp(suffix='/') -COL_HEADERS = [ - 'id', - 'doi', - 'link', - 'guid', - 'type', - 'views', - 'downloads', -] - -logger = logging.getLogger(__name__) - -keen_client = None -keen_project = KEEN['public']['project_id'] -keen_key = KEEN['public']['read_key'] -if keen_project and keen_key: - keen_client = KeenClient( - project_id=keen_project, - read_key=keen_key - ) - -def bearer_token_auth(token): - token_dict = { - 'token_type': 'Bearer', - 'access_token': token - } - return OAuth2(token=token_dict) - -def upload_metrics_file(file_path, params): - with open(file_path, 'rb') as metrics_file: - requests.put( - url=PLOS_METRICS_BASE_FOLDER, - headers={'Accept': f'application/vnd.api+json;version={DEFAULT_API_VERSION}'}, - params=params, - data=metrics_file, - auth=bearer_token_auth(PLOS_METRICS_OSF_TOKEN), - ) - -def parse_base_file(): - r = requests.get( - url=PLOS_METRICS_INITIAL_FILE_DOWNLOAD_URL, - headers={'Content-type': 'application/CSV'}, - auth=bearer_token_auth(PLOS_METRICS_OSF_TOKEN) - ) - csvr = csv.DictReader(ensure_str(r.content).splitlines()) - assert csvr.fieldnames == COL_HEADERS, f'Unexpected headers: expected {COL_HEADERS}, got {csvr.fieldnames}' - return csvr - -def fetch_metric_data_by_guid(guid): - """return: tuple(, tuple(, )) - """ - g = Guid.load(guid) - if not g: - logger.error(f'Unable to find Guid {guid}') - return None, (0, 0) - obj = Guid.load(guid).referent - if isinstance(obj, Node): - return 'node', fetch_node_metric_data(obj) - elif isinstance(obj, Registration): - return 'registration', fetch_node_metric_data(obj) - elif isinstance(obj, Preprint): - return 'preprint', fetch_preprint_metric_data(obj) - elif isinstance(obj, OsfStorageFile): - return 'file', fetch_file_metric_data(obj) - elif isinstance(obj, TrashedFile): - return 'trashedfile', fetch_file_metric_data(obj) - elif isinstance(obj, OSFUser): - return 'user', (0, 0) - else: - logger.error(f'Unknown type for {guid}') - return 'Unknown', (0, 0) - -def fetch_node_metric_data(node): - """return: tuple(, ) - """ - views = keen_client.count( - f'pageviews-{node._id[0]}', - timeframe='this_20_years', - filters=[{ - 'property_name': 'node.id', - 'operator': 'eq', - 'property_value': node._id - }] - ) - downs = 0 # AbstractNodes do not download - return views, downs - - -def fetch_preprint_metric_data(preprint): - """return: tuple(, ) - """ - views = PreprintView.get_count_for_preprint(preprint) - downs = PreprintDownload.get_count_for_preprint(preprint) - return views, downs - -def fetch_file_metric_data(file_): - """return: tuple(, ) - """ - views = file_.get_view_count() - downs = file_.get_download_count() - return views, downs - -@celery_app.task(name='osf.management.commands.cumulative_plos_metrics') -def cumulative_plos_metrics(): - today = datetime.today() - file_name = f'{today.year}_{today.month}_{today.day}_PLOS_Metrics.csv' - file_path = f'{TEMP_FOLDER}{file_name}' - logger.info('Parsing initial file...') - init_data_iter = parse_base_file() - output = io.StringIO() - writer = csv.DictWriter(output, COL_HEADERS) - writer.writeheader() - logger.info('Gathering data...') - for r in init_data_iter: - r['type'], (r['views'], r['downloads']) = fetch_metric_data_by_guid(r['guid']) - writer.writerow(r) - params = { - 'kind': 'file', - 'name': file_name - } - logger.info('Writing data...') - with open(file_path, 'w') as writeFile: - writeFile.write(output.getvalue()) - logger.info('Uploading file...') - upload_metrics_file(file_path=file_path, params=params) - logger.info('Done.') - - -class Command(BaseCommand): - def handle(self, *args, **kwargs): - start_time = datetime.now() - logger.info(f'Script start time: {start_time}') - cumulative_plos_metrics() - end_time = datetime.now() - logger.info(f'Script end time: {end_time}') diff --git a/osf/migrations/0029_remove_abstractnode_keenio_read_key.py b/osf/migrations/0029_remove_abstractnode_keenio_read_key.py new file mode 100644 index 00000000000..e9e1ca0791b --- /dev/null +++ b/osf/migrations/0029_remove_abstractnode_keenio_read_key.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.15 on 2025-02-21 13:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0028_collection_grade_levels_choices_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='abstractnode', + name='keenio_read_key', + ), + ] diff --git a/osf/models/node.py b/osf/models/node.py index d06af182e47..187f5f3123f 100644 --- a/osf/models/node.py +++ b/osf/models/node.py @@ -20,7 +20,6 @@ from django.dispatch import receiver from django.utils import timezone from django.utils.functional import cached_property -from keen import scoped_keys from psycopg2._psycopg import AsIs from typedmodels.models import TypedModel, TypedModelManager from guardian.models import ( @@ -57,7 +56,7 @@ translations as gv_translations, ) from osf.utils.datetime_aware_jsonfield import DateTimeAwareJSONField -from osf.utils.fields import NonNaiveDateTimeField, ensure_str +from osf.utils.fields import NonNaiveDateTimeField from osf.utils.requests import get_request_and_user_id, string_type_request_headers, get_current_request from osf.utils.workflows import CollectionSubmissionStates from osf.utils import sanitize @@ -479,8 +478,6 @@ def linked_nodes(self): identifiers = GenericRelation(Identifier, related_query_name='nodes') - keenio_read_key = models.CharField(max_length=1000, null=True, blank=True) - def __init__(self, *args, **kwargs): self._parent = kwargs.pop('parent', None) self._is_templated_clone = False @@ -1260,7 +1257,6 @@ def set_privacy(self, permissions, auth=None, log=True, save=True, meeting_creat self.request_embargo_termination(auth.user) return False self.is_public = True - self.keenio_read_key = self.generate_keenio_read_key() elif permissions == 'private' and self.is_public: if self.is_registration and not self.is_pending_embargo and not force: raise NodeStateError('Public registrations must be withdrawn, not made private.') @@ -1268,7 +1264,6 @@ def set_privacy(self, permissions, auth=None, log=True, save=True, meeting_creat self.check_privacy_change_viability(auth) self.is_public = False - self.keenio_read_key = '' self._remove_from_associated_collections(auth, force=force) else: return False @@ -1305,17 +1300,6 @@ def set_privacy(self, permissions, auth=None, log=True, save=True, meeting_creat project_signals.privacy_set_public.send(auth.user, node=self, meeting_creation=meeting_creation) return True - def generate_keenio_read_key(self): - encrypted_read_key = scoped_keys.encrypt(settings.KEEN['public']['master_key'], options={ - 'filters': [{ - 'property_name': 'node.id', - 'operator': 'eq', - 'property_value': str(self._id) - }], - 'allowed_operations': [READ] - }) - return ensure_str(encrypted_read_key) - @property def private_links_active(self): if self.is_spammy: diff --git a/osf_tests/test_node.py b/osf_tests/test_node.py index 4e88d1c2bf7..4fcd6e542cf 100644 --- a/osf_tests/test_node.py +++ b/osf_tests/test_node.py @@ -2254,11 +2254,9 @@ def test_set_privacy(self, node, auth): assert last_logged_before_method_call != node.last_logged node.save() assert bool(node.is_public) is True - assert node.keenio_read_key != '' node.set_privacy('private', auth=auth) node.save() assert bool(node.is_public) is False - assert node.keenio_read_key == '' assert node.logs.first().action == NodeLog.MADE_PRIVATE assert last_logged_before_method_call != node.last_logged diff --git a/package.json b/package.json index e97d5265722..b199a03a62e 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,7 @@ "js-cookie": "3.0.1", "js-md5": "^0.7.3", "jstimezonedetect": "^1.0.6", - "keen-analysis": "^1.1.0", "keen-dataviz": "^1.0.2", - "keen-tracking": "1.0.3", "knockout": "~3.4.2", "knockout.validation": "^2.0.2", "less": "^4.1.2", diff --git a/poetry.lock b/poetry.lock index e9f17c95c03..27061024317 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,141 +1,5 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. -[[package]] -name = "aiohappyeyeballs" -version = "2.4.0" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohappyeyeballs-2.4.0-py3-none-any.whl", hash = "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd"}, - {file = "aiohappyeyeballs-2.4.0.tar.gz", hash = "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2"}, -] - -[[package]] -name = "aiohttp" -version = "3.10.5" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3"}, - {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6"}, - {file = "aiohttp-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683"}, - {file = "aiohttp-3.10.5-cp310-cp310-win32.whl", hash = "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef"}, - {file = "aiohttp-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058"}, - {file = "aiohttp-3.10.5-cp311-cp311-win32.whl", hash = "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072"}, - {file = "aiohttp-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6"}, - {file = "aiohttp-3.10.5-cp312-cp312-win32.whl", hash = "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12"}, - {file = "aiohttp-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987"}, - {file = "aiohttp-3.10.5-cp313-cp313-win32.whl", hash = "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04"}, - {file = "aiohttp-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f6f18898ace4bcd2d41a122916475344a87f1dfdec626ecde9ee802a711bc569"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5ede29d91a40ba22ac1b922ef510aab871652f6c88ef60b9dcdf773c6d32ad7a"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:673f988370f5954df96cc31fd99c7312a3af0a97f09e407399f61583f30da9bc"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58718e181c56a3c02d25b09d4115eb02aafe1a732ce5714ab70326d9776457c3"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b38b1570242fbab8d86a84128fb5b5234a2f70c2e32f3070143a6d94bc854cf"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:074d1bff0163e107e97bd48cad9f928fa5a3eb4b9d33366137ffce08a63e37fe"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7384d0b87d4635ec38db9263e6a3f1eb609e2e06087f0aa7f63b76833737b471"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8989f46f3d7ef79585e98fa991e6ded55d2f48ae56d2c9fa5e491a6e4effb589"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c83f7a107abb89a227d6c454c613e7606c12a42b9a4ca9c5d7dad25d47c776ae"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cde98f323d6bf161041e7627a5fd763f9fd829bcfcd089804a5fdce7bb6e1b7d"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:676f94c5480d8eefd97c0c7e3953315e4d8c2b71f3b49539beb2aa676c58272f"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2d21ac12dc943c68135ff858c3a989f2194a709e6e10b4c8977d7fcd67dfd511"}, - {file = "aiohttp-3.10.5-cp38-cp38-win32.whl", hash = "sha256:17e997105bd1a260850272bfb50e2a328e029c941c2708170d9d978d5a30ad9a"}, - {file = "aiohttp-3.10.5-cp38-cp38-win_amd64.whl", hash = "sha256:1c19de68896747a2aa6257ae4cf6ef59d73917a36a35ee9d0a6f48cff0f94db8"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e2fe37ac654032db1f3499fe56e77190282534810e2a8e833141a021faaab0e"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5bf3ead3cb66ab990ee2561373b009db5bc0e857549b6c9ba84b20bc462e172"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b2c16a919d936ca87a3c5f0e43af12a89a3ce7ccbce59a2d6784caba945b68b"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad146dae5977c4dd435eb31373b3fe9b0b1bf26858c6fc452bf6af394067e10b"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c5c6fa16412b35999320f5c9690c0f554392dc222c04e559217e0f9ae244b92"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95c4dc6f61d610bc0ee1edc6f29d993f10febfe5b76bb470b486d90bbece6b22"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da452c2c322e9ce0cfef392e469a26d63d42860f829026a63374fde6b5c5876f"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:898715cf566ec2869d5cb4d5fb4be408964704c46c96b4be267442d265390f32"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:391cc3a9c1527e424c6865e087897e766a917f15dddb360174a70467572ac6ce"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:380f926b51b92d02a34119d072f178d80bbda334d1a7e10fa22d467a66e494db"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce91db90dbf37bb6fa0997f26574107e1b9d5ff939315247b7e615baa8ec313b"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9093a81e18c45227eebe4c16124ebf3e0d893830c6aca7cc310bfca8fe59d857"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ee40b40aa753d844162dcc80d0fe256b87cba48ca0054f64e68000453caead11"}, - {file = "aiohttp-3.10.5-cp39-cp39-win32.whl", hash = "sha256:03f2645adbe17f274444953bdea69f8327e9d278d961d85657cb0d06864814c1"}, - {file = "aiohttp-3.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:d17920f18e6ee090bdd3d0bfffd769d9f2cb4c8ffde3eb203777a3895c128862"}, - {file = "aiohttp-3.10.5.tar.gz", hash = "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.3.0" -aiosignal = ">=1.1.2" -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" - [[package]] name = "amqp" version = "5.2.0" @@ -1249,7 +1113,6 @@ python-versions = "*" files = [ {file = "django-sendgrid-v5-1.2.3.tar.gz", hash = "sha256:3887aafbb10d5b808efc2c1031dcd96fd357d542eb5affe38fef07cc0f3cfae9"}, {file = "django_sendgrid_v5-1.2.3-py2.py3-none-any.whl", hash = "sha256:2d2fa8a085d21c95e5f97fc60b61f199ccc57a27df8da90cd3f29a5702346dc6"}, - {file = "django_sendgrid_v5-1.2.3-py3-none-any.whl", hash = "sha256:f6a44ee37c1c3cc7d683a43c55ead530417be1849a8a41bde02b158009559d9d"}, ] [package.dependencies] @@ -1694,92 +1557,6 @@ Werkzeug = ">=3.0.0" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] -[[package]] -name = "frozenlist" -version = "1.4.1" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.8" -files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, -] - [[package]] name = "furl" version = "2.1.3" @@ -1795,22 +1572,6 @@ files = [ orderedmultidict = ">=1.0.1" six = ">=1.8.0" -[[package]] -name = "geoip2" -version = "4.7.0" -description = "MaxMind GeoIP2 API" -optional = false -python-versions = ">=3.7" -files = [ - {file = "geoip2-4.7.0-py2.py3-none-any.whl", hash = "sha256:078fcd4cce26ea029b1e3252a0f0ec20a1f42e7ab0f19b7be3864f20f4db2b51"}, - {file = "geoip2-4.7.0.tar.gz", hash = "sha256:3bdde4994f6bc917eafab5b51e772d737b2ae00037a5b85001fb06dc68f779df"}, -] - -[package.dependencies] -aiohttp = ">=3.6.2,<4.0.0" -maxminddb = ">=2.3.0,<3.0.0" -requests = ">=2.24.0,<3.0.0" - [[package]] name = "gevent" version = "24.2.1" @@ -2450,21 +2211,6 @@ files = [ [package.dependencies] referencing = ">=0.31.0" -[[package]] -name = "keen" -version = "0.7.0" -description = "Python Client for Keen IO" -optional = false -python-versions = "*" -files = [ - {file = "keen-0.7.0.tar.gz", hash = "sha256:141fad22abd52dbe9e84a21866a7affcbc8b46f04885a5598618c5992bd6667c"}, -] - -[package.dependencies] -pycryptodome = ">=3.4" -requests = ">=2.5,<3.0" -six = ">=1.10,<2.0" - [[package]] name = "kombu" version = "5.3.5" @@ -2735,84 +2481,6 @@ files = [ [package.dependencies] traitlets = "*" -[[package]] -name = "maxminddb" -version = "2.6.2" -description = "Reader for the MaxMind DB format" -optional = false -python-versions = ">=3.8" -files = [ - {file = "maxminddb-2.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cfdf5c29a2739610700b9fea7f8d68ce81dcf30bb8016f1a1853ef889a2624b"}, - {file = "maxminddb-2.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05e873eb82281cef6e787bd40bd1d58b2e496a21b3689346f0d0420988b3cbb1"}, - {file = "maxminddb-2.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2b85ffc9fb2e192321c2f0b34d0b291b8e82de6e51a6ec7534645663678e835"}, - {file = "maxminddb-2.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a2eaf9769262c05c486e777016771f3367c843b053c43cd5fde1108755753d"}, - {file = "maxminddb-2.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96a1fa38322bce1d587bb6ce39a0e6ca4c1b824f48fbc5739a5ec507f63aa889"}, - {file = "maxminddb-2.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eb534333f5fd7180e35c0207b3d95d621e4b9be3b8c1709995d0feb6c752b6f4"}, - {file = "maxminddb-2.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b281c0eec3601dde1f169a1c04e2615751c66368141aded9f03131fe635450b"}, - {file = "maxminddb-2.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a771df92e599ad867c16ae4acb08cc3763c9d1028f4ca772c0571da97f7f86d2"}, - {file = "maxminddb-2.6.2-cp310-cp310-win32.whl", hash = "sha256:f412a54f87ef9083911c334267188d3d1b14f2591eac94b94ca32528f21d5f25"}, - {file = "maxminddb-2.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:7e5a90a1cb0c7fd6226aa44e18a87b26fa85b6eebae36d529d7582f93e8dfbd1"}, - {file = "maxminddb-2.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38941a38278491bf95e5ca544969782c7ab33326802f6a93816867289c3f6401"}, - {file = "maxminddb-2.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eef1c26210155c7b94c4ca28fef65eb44a5ca1584427b1fbdeec1cd3c81e25c5"}, - {file = "maxminddb-2.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4d9cd7ddd02ee123a44d0d7821166d31540ea85352deb06b29d55e802f32781"}, - {file = "maxminddb-2.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8101291e5b92bd272a050c25822a5e30860d453dde16b4fffed9d751f0483a82"}, - {file = "maxminddb-2.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c7c520d06d335b288d06a00b786cea9b7e023bd588efb1a6ef485e94ccc7244"}, - {file = "maxminddb-2.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58bfd2c55c96aaaa7c4996c704edabfb1bd369dfc1592cedf8957a24062178b1"}, - {file = "maxminddb-2.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:886af3ba4aa26214ff39214565f53152b62a5abdb6ef9e00c76c194dbfd79231"}, - {file = "maxminddb-2.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:93691c8b4b4c448babb37bedc6f3d51523a3f06ab11bdd171da7ffc4005a7897"}, - {file = "maxminddb-2.6.2-cp311-cp311-win32.whl", hash = "sha256:e9013076deca5d136c260510cd05e82ec2b4ddb9476d63e2180a13ddfd305c3e"}, - {file = "maxminddb-2.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:47170ec0e1e76787cc5882301c487f495d67f3146318f2f4e2adc281951a96ef"}, - {file = "maxminddb-2.6.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eacd65e38bdf4efdf42bbc15cfa734b09eb818ecfef76b7b36e64be382be4c83"}, - {file = "maxminddb-2.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:20662878bc9514e90b0b4c4eb1a76622ecc7504d012e76bad9cdb7372fc0ef96"}, - {file = "maxminddb-2.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7607e45f7eca991fa34d57c03a791a1dfbe774ddd9250d0f35cdcc6f17142a15"}, - {file = "maxminddb-2.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0970b661c4fac6624b9128057ed5fe35a2d95aa60359272289cd4c7207c9a6d"}, - {file = "maxminddb-2.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12207f0becf3f2bf14e7a4bf86efcaa6e90d665a918915ae228c4e77792d7151"}, - {file = "maxminddb-2.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:826a1858b93b193df7fa71e3caca65c3051db20545df0020444f55c02e8ed2c3"}, - {file = "maxminddb-2.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e63649a82926f1d93acdd3df5f7be66dc9473653350afe73f365bb25e5b34368"}, - {file = "maxminddb-2.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ebf9fdf8a8e55862aabb8b2c34a4af31a8a5b686007288eeb561fa20ef348378"}, - {file = "maxminddb-2.6.2-cp312-cp312-win32.whl", hash = "sha256:2aaefb62f881151960bb67e5aeb302c159a32bd2d623cf72dad688bda1020869"}, - {file = "maxminddb-2.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:78c3aa70c62be68ace23f819e7f23258545f2bfbd92cd6c33ee398cd261f6b84"}, - {file = "maxminddb-2.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e1e40449bd278fdca1f351df442f391e72fd3d98b054ccac1672f27d70210642"}, - {file = "maxminddb-2.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:80d7f943f6b8bc437eaae5da778a83d8f38e4b7463756fdee04833e1be0bdea2"}, - {file = "maxminddb-2.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:058ca89789bc1770fe58d02a88272ca91dabeef9f3fe0011fe506484355f1804"}, - {file = "maxminddb-2.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80d20683afe01b4d41bad1c1829f87ab12f3d19c68ec230f83318a2fd13871a7"}, - {file = "maxminddb-2.6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd90c3798e6c347d48d5d9a9c95dc678b52a5a965f1fb72152067fdf52b994da"}, - {file = "maxminddb-2.6.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:add1e55620033516c5f0734b1d9d03848859192d9f3825aabe720dfa8a783958"}, - {file = "maxminddb-2.6.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:8cb992da535264177b380e7b81943c884d57dcbfad6b3335d7f633967144746e"}, - {file = "maxminddb-2.6.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:86048ff328793599e584bcc2fc8278c2b7c5d3a4005c70403613449ec93817ef"}, - {file = "maxminddb-2.6.2-cp38-cp38-win32.whl", hash = "sha256:f2e326a99eaa924ff2fb09d6e44127983a43016228e7780888f15e9ba171d7b3"}, - {file = "maxminddb-2.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a2671e8f4161130803cf226cd9cb8b93ec5c4b2493f83a902986177052d95d3"}, - {file = "maxminddb-2.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a50bc348c699d8f6a5f0aa35e5096515d642ca2f38b944bd71c3dedda3d3588"}, - {file = "maxminddb-2.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc9f1203eb2b139252aa08965960fe13c36cc8b80b536490b94b05c31aa1fca9"}, - {file = "maxminddb-2.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ccca5327cb4e706f669456ec6d556badfa92c0fdacd57a15076f3cdc061560"}, - {file = "maxminddb-2.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3987e103396e925edebbef4877e94515822f63b3b436027a0b164b500622fccd"}, - {file = "maxminddb-2.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b31ecf3083b78c77624783bfdf6177e6ac73ae14684ef182855eb5569bc78e7c"}, - {file = "maxminddb-2.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:cd4530b9604d66cfa5e37eb94c671e54feff87769f8ba7fa997cce959e0cb241"}, - {file = "maxminddb-2.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ecce0b2d125691e2311f94dbd564c2d61c36c5033d082919431a21e6c694fa3f"}, - {file = "maxminddb-2.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34b6e8d667d724f60d52635f3d959f793ab4e5d57d78b27fe66f02752d8c6b08"}, - {file = "maxminddb-2.6.2-cp39-cp39-win32.whl", hash = "sha256:d15414d251513748cb646d284a2829a5f4c69d8c90963a6e6da53a1a6d0accf7"}, - {file = "maxminddb-2.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:7c1220838ba9b0bcdaa0c5846f9da70a2304df2ac255fe518370f8faf8c18316"}, - {file = "maxminddb-2.6.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39eab93ddd75fd02f8d5ad6b1bd3f8d894828d91d6f6c1a96bb9e87c34e94aaa"}, - {file = "maxminddb-2.6.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa8cb54b01a29a23a0ea6659fbb38deec6f35453588c5decdbf8669feb53b624"}, - {file = "maxminddb-2.6.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c096dfd20926c4de7d7fd5b5e75c756eddd4bdac5ab7aafd4bb67d000b13743"}, - {file = "maxminddb-2.6.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dc2b511c7255f7cbbb01e8ba01ba82e62e9c1213e382d36f9d9b0ee45c2f6b2"}, - {file = "maxminddb-2.6.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80d7495565d30260c630afbe74d61522b13dd31ed05b8916003ec5b127109a12"}, - {file = "maxminddb-2.6.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9dccd7a438f81e3df84dfc31a75af4c8d29adefb6082329385bfde604c9ea01b"}, - {file = "maxminddb-2.6.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b0a3b9cab1a94cc633df3da85c6567f0188f10165e3338ec9a6c421de9fe53b9"}, - {file = "maxminddb-2.6.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb38aa94e76a87785b654c035f9f3ee39b74a98e9beea9a10b1aa62abdcc4cbd"}, - {file = "maxminddb-2.6.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9e9e893f7c0fa44cfdd5ab819a07d93f63ee398c28b792cedd50b94dcfea7c0"}, - {file = "maxminddb-2.6.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28af9470f28fce2ccb945478235f53fb52d98a505653b1bf4028e34df6149a06"}, - {file = "maxminddb-2.6.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a74b60cdc61a69b967ec44201c6259fbc48ef2eab2e885fbdc50ec1accaad545"}, - {file = "maxminddb-2.6.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:485c0778f6801e1437c2efd6e3b964a7ae71c8819f063e0b5460c3267d977040"}, - {file = "maxminddb-2.6.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0b480a31589750da4e36d1ba04b77ee3ac3853ac7b94d63f337b9d4d0403043f"}, - {file = "maxminddb-2.6.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:85fc9406f42c1311ce8ea9f2c820db5d7ac687a39ab5d932708dc783607378ef"}, - {file = "maxminddb-2.6.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fd1a612110ff182a559d8010e7615e5d05ef9d2c234b5f7de124ee8fdf1ecb9"}, - {file = "maxminddb-2.6.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cd7f525eb2331cf05181c5ba562cc3edec3de4b41dbb18a5fee9ad24884b499"}, - {file = "maxminddb-2.6.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d32266792b349f5507b0369d3277d45318fcd346a16dcc98b484aadc208e4d74"}, - {file = "maxminddb-2.6.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5662386db91872d5505fde9e7bb0b9530b6aab7a6f3ece7df59a2b43a7b45d17"}, - {file = "maxminddb-2.6.2.tar.gz", hash = "sha256:7d842d32e2620abc894b7d79a5a1007a69df2c6cf279a06b94c9c3913f66f264"}, -] - [[package]] name = "mccabe" version = "0.7.0" @@ -2929,107 +2597,6 @@ files = [ {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, ] -[[package]] -name = "multidict" -version = "6.1.0" -description = "multidict implementation" -optional = false -python-versions = ">=3.8" -files = [ - {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, - {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, - {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, - {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, - {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, - {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, - {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, - {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, - {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, - {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, - {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, - {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, - {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, - {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, - {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, - {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, - {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, -] - [[package]] name = "nameparser" version = "1.1.3" @@ -3468,47 +3035,6 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -[[package]] -name = "pycryptodome" -version = "3.20.0" -description = "Cryptographic library for Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, - {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, -] - [[package]] name = "pydevd" version = "3.0.3" @@ -4894,111 +4420,6 @@ markupsafe = "*" [package.extras] email = ["email-validator"] -[[package]] -name = "yarl" -version = "1.11.1" -description = "Yet another URL library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "yarl-1.11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00"}, - {file = "yarl-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d"}, - {file = "yarl-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2164cd9725092761fed26f299e3f276bb4b537ca58e6ff6b252eae9631b5c96e"}, - {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08ea567c16f140af8ddc7cb58e27e9138a1386e3e6e53982abaa6f2377b38cc"}, - {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:768ecc550096b028754ea28bf90fde071c379c62c43afa574edc6f33ee5daaec"}, - {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2909fa3a7d249ef64eeb2faa04b7957e34fefb6ec9966506312349ed8a7e77bf"}, - {file = "yarl-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01a8697ec24f17c349c4f655763c4db70eebc56a5f82995e5e26e837c6eb0e49"}, - {file = "yarl-1.11.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e286580b6511aac7c3268a78cdb861ec739d3e5a2a53b4809faef6b49778eaff"}, - {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4179522dc0305c3fc9782549175c8e8849252fefeb077c92a73889ccbcd508ad"}, - {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:27fcb271a41b746bd0e2a92182df507e1c204759f460ff784ca614e12dd85145"}, - {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f61db3b7e870914dbd9434b560075e0366771eecbe6d2b5561f5bc7485f39efd"}, - {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c92261eb2ad367629dc437536463dc934030c9e7caca861cc51990fe6c565f26"}, - {file = "yarl-1.11.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d95b52fbef190ca87d8c42f49e314eace4fc52070f3dfa5f87a6594b0c1c6e46"}, - {file = "yarl-1.11.1-cp310-cp310-win32.whl", hash = "sha256:489fa8bde4f1244ad6c5f6d11bb33e09cf0d1d0367edb197619c3e3fc06f3d91"}, - {file = "yarl-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:476e20c433b356e16e9a141449f25161e6b69984fb4cdbd7cd4bd54c17844998"}, - {file = "yarl-1.11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:946eedc12895873891aaceb39bceb484b4977f70373e0122da483f6c38faaa68"}, - {file = "yarl-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:21a7c12321436b066c11ec19c7e3cb9aec18884fe0d5b25d03d756a9e654edfe"}, - {file = "yarl-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c35f493b867912f6fda721a59cc7c4766d382040bdf1ddaeeaa7fa4d072f4675"}, - {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25861303e0be76b60fddc1250ec5986c42f0a5c0c50ff57cc30b1be199c00e63"}, - {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4b53f73077e839b3f89c992223f15b1d2ab314bdbdf502afdc7bb18e95eae27"}, - {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:327c724b01b8641a1bf1ab3b232fb638706e50f76c0b5bf16051ab65c868fac5"}, - {file = "yarl-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4307d9a3417eea87715c9736d050c83e8c1904e9b7aada6ce61b46361b733d92"}, - {file = "yarl-1.11.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a28bed68ab8fb7e380775f0029a079f08a17799cb3387a65d14ace16c12e2b"}, - {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:067b961853c8e62725ff2893226fef3d0da060656a9827f3f520fb1d19b2b68a"}, - {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8215f6f21394d1f46e222abeb06316e77ef328d628f593502d8fc2a9117bde83"}, - {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:498442e3af2a860a663baa14fbf23fb04b0dd758039c0e7c8f91cb9279799bff"}, - {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:69721b8effdb588cb055cc22f7c5105ca6fdaa5aeb3ea09021d517882c4a904c"}, - {file = "yarl-1.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e969fa4c1e0b1a391f3fcbcb9ec31e84440253325b534519be0d28f4b6b533e"}, - {file = "yarl-1.11.1-cp311-cp311-win32.whl", hash = "sha256:7d51324a04fc4b0e097ff8a153e9276c2593106a811704025bbc1d6916f45ca6"}, - {file = "yarl-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:15061ce6584ece023457fb8b7a7a69ec40bf7114d781a8c4f5dcd68e28b5c53b"}, - {file = "yarl-1.11.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a4264515f9117be204935cd230fb2a052dd3792789cc94c101c535d349b3dab0"}, - {file = "yarl-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f41fa79114a1d2eddb5eea7b912d6160508f57440bd302ce96eaa384914cd265"}, - {file = "yarl-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:02da8759b47d964f9173c8675710720b468aa1c1693be0c9c64abb9d8d9a4867"}, - {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9361628f28f48dcf8b2f528420d4d68102f593f9c2e592bfc842f5fb337e44fd"}, - {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b91044952da03b6f95fdba398d7993dd983b64d3c31c358a4c89e3c19b6f7aef"}, - {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74db2ef03b442276d25951749a803ddb6e270d02dda1d1c556f6ae595a0d76a8"}, - {file = "yarl-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e975a2211952a8a083d1b9d9ba26472981ae338e720b419eb50535de3c02870"}, - {file = "yarl-1.11.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aef97ba1dd2138112890ef848e17d8526fe80b21f743b4ee65947ea184f07a2"}, - {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7915ea49b0c113641dc4d9338efa9bd66b6a9a485ffe75b9907e8573ca94b84"}, - {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:504cf0d4c5e4579a51261d6091267f9fd997ef58558c4ffa7a3e1460bd2336fa"}, - {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de5292f9f0ee285e6bd168b2a77b2a00d74cbcfa420ed078456d3023d2f6dff"}, - {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a34e1e30f1774fa35d37202bbeae62423e9a79d78d0874e5556a593479fdf239"}, - {file = "yarl-1.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66b63c504d2ca43bf7221a1f72fbe981ff56ecb39004c70a94485d13e37ebf45"}, - {file = "yarl-1.11.1-cp312-cp312-win32.whl", hash = "sha256:a28b70c9e2213de425d9cba5ab2e7f7a1c8ca23a99c4b5159bf77b9c31251447"}, - {file = "yarl-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:17b5a386d0d36fb828e2fb3ef08c8829c1ebf977eef88e5367d1c8c94b454639"}, - {file = "yarl-1.11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1fa2e7a406fbd45b61b4433e3aa254a2c3e14c4b3186f6e952d08a730807fa0c"}, - {file = "yarl-1.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:750f656832d7d3cb0c76be137ee79405cc17e792f31e0a01eee390e383b2936e"}, - {file = "yarl-1.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b8486f322d8f6a38539136a22c55f94d269addb24db5cb6f61adc61eabc9d93"}, - {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fce4da3703ee6048ad4138fe74619c50874afe98b1ad87b2698ef95bf92c96d"}, - {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed653638ef669e0efc6fe2acb792275cb419bf9cb5c5049399f3556995f23c7"}, - {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18ac56c9dd70941ecad42b5a906820824ca72ff84ad6fa18db33c2537ae2e089"}, - {file = "yarl-1.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:688654f8507464745ab563b041d1fb7dab5d9912ca6b06e61d1c4708366832f5"}, - {file = "yarl-1.11.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4973eac1e2ff63cf187073cd4e1f1148dcd119314ab79b88e1b3fad74a18c9d5"}, - {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:964a428132227edff96d6f3cf261573cb0f1a60c9a764ce28cda9525f18f7786"}, - {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6d23754b9939cbab02c63434776df1170e43b09c6a517585c7ce2b3d449b7318"}, - {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c2dc4250fe94d8cd864d66018f8344d4af50e3758e9d725e94fecfa27588ff82"}, - {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09696438cb43ea6f9492ef237761b043f9179f455f405279e609f2bc9100212a"}, - {file = "yarl-1.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:999bfee0a5b7385a0af5ffb606393509cfde70ecca4f01c36985be6d33e336da"}, - {file = "yarl-1.11.1-cp313-cp313-win32.whl", hash = "sha256:ce928c9c6409c79e10f39604a7e214b3cb69552952fbda8d836c052832e6a979"}, - {file = "yarl-1.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:501c503eed2bb306638ccb60c174f856cc3246c861829ff40eaa80e2f0330367"}, - {file = "yarl-1.11.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dae7bd0daeb33aa3e79e72877d3d51052e8b19c9025ecf0374f542ea8ec120e4"}, - {file = "yarl-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3ff6b1617aa39279fe18a76c8d165469c48b159931d9b48239065767ee455b2b"}, - {file = "yarl-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3257978c870728a52dcce8c2902bf01f6c53b65094b457bf87b2644ee6238ddc"}, - {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f351fa31234699d6084ff98283cb1e852270fe9e250a3b3bf7804eb493bd937"}, - {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8aef1b64da41d18026632d99a06b3fefe1d08e85dd81d849fa7c96301ed22f1b"}, - {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7175a87ab8f7fbde37160a15e58e138ba3b2b0e05492d7351314a250d61b1591"}, - {file = "yarl-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba444bdd4caa2a94456ef67a2f383710928820dd0117aae6650a4d17029fa25e"}, - {file = "yarl-1.11.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ea9682124fc062e3d931c6911934a678cb28453f957ddccf51f568c2f2b5e05"}, - {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8418c053aeb236b20b0ab8fa6bacfc2feaaf7d4683dd96528610989c99723d5f"}, - {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:61a5f2c14d0a1adfdd82258f756b23a550c13ba4c86c84106be4c111a3a4e413"}, - {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f3a6d90cab0bdf07df8f176eae3a07127daafcf7457b997b2bf46776da2c7eb7"}, - {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:077da604852be488c9a05a524068cdae1e972b7dc02438161c32420fb4ec5e14"}, - {file = "yarl-1.11.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:15439f3c5c72686b6c3ff235279630d08936ace67d0fe5c8d5bbc3ef06f5a420"}, - {file = "yarl-1.11.1-cp38-cp38-win32.whl", hash = "sha256:238a21849dd7554cb4d25a14ffbfa0ef380bb7ba201f45b144a14454a72ffa5a"}, - {file = "yarl-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:67459cf8cf31da0e2cbdb4b040507e535d25cfbb1604ca76396a3a66b8ba37a6"}, - {file = "yarl-1.11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:884eab2ce97cbaf89f264372eae58388862c33c4f551c15680dd80f53c89a269"}, - {file = "yarl-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a336eaa7ee7e87cdece3cedb395c9657d227bfceb6781295cf56abcd3386a26"}, - {file = "yarl-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87f020d010ba80a247c4abc335fc13421037800ca20b42af5ae40e5fd75e7909"}, - {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:637c7ddb585a62d4469f843dac221f23eec3cbad31693b23abbc2c366ad41ff4"}, - {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48dfd117ab93f0129084577a07287376cc69c08138694396f305636e229caa1a"}, - {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e0ae31fb5ccab6eda09ba1494e87eb226dcbd2372dae96b87800e1dcc98804"}, - {file = "yarl-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f46f81501160c28d0c0b7333b4f7be8983dbbc161983b6fb814024d1b4952f79"}, - {file = "yarl-1.11.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04293941646647b3bfb1719d1d11ff1028e9c30199509a844da3c0f5919dc520"}, - {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:250e888fa62d73e721f3041e3a9abf427788a1934b426b45e1b92f62c1f68366"}, - {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e8f63904df26d1a66aabc141bfd258bf738b9bc7bc6bdef22713b4f5ef789a4c"}, - {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:aac44097d838dda26526cffb63bdd8737a2dbdf5f2c68efb72ad83aec6673c7e"}, - {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:267b24f891e74eccbdff42241c5fb4f974de2d6271dcc7d7e0c9ae1079a560d9"}, - {file = "yarl-1.11.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6907daa4b9d7a688063ed098c472f96e8181733c525e03e866fb5db480a424df"}, - {file = "yarl-1.11.1-cp39-cp39-win32.whl", hash = "sha256:14438dfc5015661f75f85bc5adad0743678eefee266ff0c9a8e32969d5d69f74"}, - {file = "yarl-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:94d0caaa912bfcdc702a4204cd5e2bb01eb917fc4f5ea2315aa23962549561b0"}, - {file = "yarl-1.11.1-py3-none-any.whl", hash = "sha256:72bf26f66456baa0584eff63e44545c9f0eaed9b73cb6601b647c91f14c11f38"}, - {file = "yarl-1.11.1.tar.gz", hash = "sha256:1bb2d9e212fb7449b8fb73bc461b51eaa17cc8430b4a87d87be7b25052d92f53"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" - [[package]] name = "zope-event" version = "5.0" @@ -5071,4 +4492,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "26b5234a40ced450784309969babc41b8cd042e2c6eccda2958d93e978cb5dc6" +content-hash = "81b3fc071f1be070d1072d4cfe1a45c8c44815e803c4ba17cf6da85a6b7b3894" diff --git a/pyproject.toml b/pyproject.toml index 51805194f39..5e74a6defb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,9 +75,6 @@ django-guardian = "2.4.0" django-webpack-loader = {git = "https://github.com/CenterForOpenScience/django-webpack-loader.git", rev = "6b62fef7d6bc9d25d7b7b7a303f4580ad24831a6"} # branch is feature/v1-webpack-stats django-sendgrid-v5 = "1.2.3" # metadata says python 3.10 not supported, but tests pass -# Analytics requirements -keen = "0.7.0" -geoip2 = "4.7.0" # OSF models django-typed-models = "0.14.0" django-storages = "1.14.3" diff --git a/scripts/populate_popular_projects_and_registrations.py b/scripts/populate_popular_projects_and_registrations.py deleted file mode 100644 index 0bf52709d19..00000000000 --- a/scripts/populate_popular_projects_and_registrations.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -This will update node links on POPULAR_LINKS_NODE, POPULAR_LINKS_REGISTRATIONS and NEW_AND_NOTEWORTHY_LINKS_NODE. -""" -import sys -import logging - -from django.db import transaction - -from website.app import init_app -from framework.auth.core import Auth -from scripts import utils as script_utils -from framework.celery_tasks import app as celery_app -from website.settings import POPULAR_LINKS_NODE, POPULAR_LINKS_REGISTRATIONS - -logger = logging.getLogger(__name__) - - -def update_node_links(designated_node, target_nodes, description): - """ Takes designated node, removes current node links and replaces them with node links to target nodes """ - if len(target_nodes) == 0: - logger.info('No target nodes specified - no node links will be added!') - else: - logger.info(f'Repopulating {designated_node._id} with latest {description} nodes.') - user = designated_node.creator - auth = Auth(user) - - for pointer in designated_node.nodes_pointer: - designated_node.rm_pointer(pointer, auth) - - for node in target_nodes: - designated_node.add_pointer(node, auth, save=True) - logger.info(f'Added node link {node} to {designated_node}') - -def main(dry_run=True): - init_app(routes=False) - from osf.models import AbstractNode - from website.project.utils import activity - - popular_activity = activity() - - popular_nodes = popular_activity['popular_public_projects'] - popular_links_node = AbstractNode.objects.get(guids___id=POPULAR_LINKS_NODE, guids___id__isnull=False) - popular_registrations = popular_activity['popular_public_registrations'] - popular_links_registrations = AbstractNode.objects.get(guids___id=POPULAR_LINKS_REGISTRATIONS) - - update_node_links(popular_links_node, popular_nodes, 'popular') - update_node_links(popular_links_registrations, popular_registrations, 'popular registrations') - try: - popular_links_node.save() - logger.info(f'Node links on {popular_links_node._id} updated.') - except (KeyError, RuntimeError) as error: - logger.error('Could not migrate popular nodes due to error') - logger.exception(error) - - try: - popular_links_registrations.save() - logger.info(f'Node links for registrations on {popular_links_registrations._id} updated.') - except (KeyError, RuntimeError) as error: - logger.error('Could not migrate popular nodes for registrations due to error') - logger.exception(error) - - if dry_run: - raise RuntimeError('Dry run -- transaction rolled back.') - - -@celery_app.task(name='scripts.populate_popular_projects_and_registrations') -def run_main(dry_run=True): - if not dry_run: - script_utils.add_file_logger(logger, __file__) - with transaction.atomic(): - main(dry_run=dry_run) - -if __name__ == '__main__': - dry_run = '--dry' in sys.argv - run_main(dry_run=dry_run) diff --git a/scripts/tests/test_populate_popular_projects_and_registrations.py b/scripts/tests/test_populate_popular_projects_and_registrations.py deleted file mode 100644 index b1cf38c651d..00000000000 --- a/scripts/tests/test_populate_popular_projects_and_registrations.py +++ /dev/null @@ -1,95 +0,0 @@ -from unittest import mock - -from tests.base import OsfTestCase -from osf_tests.factories import ProjectFactory, RegistrationFactory - -from website.settings import POPULAR_LINKS_NODE, POPULAR_LINKS_REGISTRATIONS, NEW_AND_NOTEWORTHY_LINKS_NODE - -from scripts import populate_popular_projects_and_registrations as script - - -class TestPopulateNewAndNoteworthy(OsfTestCase): - - def setUp(self): - super().setUp() - self.pop1 = ProjectFactory(is_public=True) - self.pop2 = ProjectFactory(is_public=True) - - self.popreg1 = RegistrationFactory(is_public=True) - self.popreg2 = RegistrationFactory(is_public=True) - - def tearDown(self): - super().tearDown() - - @mock.patch('website.project.utils.get_keen_activity') - def test_populate_popular_nodes_and_registrations(self, mock_client): - - # only for setup, not used - self.new_noteworthy_node = ProjectFactory() - self.new_noteworthy_node._id = NEW_AND_NOTEWORTHY_LINKS_NODE - self.new_noteworthy_node.save() - - self.popular_links_node = ProjectFactory() - self.popular_links_node._id = POPULAR_LINKS_NODE - self.popular_links_node.save() - - self.popular_links_registrations = ProjectFactory() - self.popular_links_registrations._id = POPULAR_LINKS_REGISTRATIONS - self.popular_links_registrations.save() - - popular_nodes = [self.pop1, self.pop2] - popular_registrations = [self.popreg1, self.popreg2] - - node_pageviews = [ - { - 'result': 5, - 'node.id': self.pop1._id - }, - { - 'result': 5, - 'node.id': self.pop2._id - }, - { - 'result': 5, - 'node.id': self.popreg1._id - }, - { - 'result': 5, - 'node.id': self.popreg2._id - } - ] - - node_visits = [ - { - 'result': 2, - 'node.id': self.pop1._id - }, - { - 'result': 2, - 'node.id': self.pop2._id - }, - { - 'result': 2, - 'node.id': self.popreg1._id - }, - { - 'result': 2, - 'node.id': self.popreg2._id - } - ] - - mock_client.return_value = {'node_pageviews': node_pageviews, 'node_visits': node_visits} - - assert len(self.popular_links_node.nodes) == 0 - assert len(self.popular_links_registrations.nodes) == 0 - - script.main(dry_run=False) - - self.popular_links_node.reload() - self.popular_links_registrations.reload() - - assert len(self.popular_links_node.nodes) == 2 - assert len(self.popular_links_registrations.nodes) == 2 - - assert popular_nodes == self.popular_links_node.nodes - assert popular_registrations == self.popular_links_registrations.nodes diff --git a/website/project/utils.py b/website/project/utils.py index 173219d5558..e8eb0eaf526 100644 --- a/website/project/utils.py +++ b/website/project/utils.py @@ -1,14 +1,8 @@ """Various node-related utilities.""" -from string import ascii_lowercase, digits - from django.apps import apps from django.db.models import Q from website import settings - -from keen import KeenClient - - # Alias the project serializer def serialize_node(*args, **kwargs): @@ -28,47 +22,6 @@ def recent_public_registrations(n=10): ).get_roots().order_by('-registered_date')[:n] -def get_keen_activity(): - client = KeenClient( - project_id=settings.KEEN['public']['project_id'], - read_key=settings.KEEN['public']['read_key'], - ) - - node_pageviews = [] - node_visits = [] - for character in list(digits + ascii_lowercase): - partial_node_pageviews = client.count( - event_collection=f'pageviews-{character}', - timeframe='this_7_days', - group_by='node.id', - filters=[ - { - 'property_name': 'node.id', - 'operator': 'exists', - 'property_value': True - } - ] - ) - node_pageviews += partial_node_pageviews - - partial_node_visits = client.count_unique( - event_collection=f'pageviews-{character}', - target_property='anon.id', - timeframe='this_7_days', - group_by='node.id', - filters=[ - { - 'property_name': 'node.id', - 'operator': 'exists', - 'property_value': True - } - ] - ) - node_visits += partial_node_visits - - return {'node_pageviews': node_pageviews, 'node_visits': node_visits} - - def activity(): """Generate analytics for most popular public projects and registrations. Called by `scripts/update_populate_projects_and_registrations` @@ -76,33 +29,8 @@ def activity(): Node = apps.get_model('osf.AbstractNode') popular_public_projects = [] popular_public_registrations = [] - max_projects_to_display = settings.MAX_POPULAR_PROJECTS - - if settings.KEEN['public']['read_key']: - keen_activity = get_keen_activity() - node_visits = keen_activity['node_visits'] - - node_data = [{'node': x['node.id'], 'views': x['result']} for x in node_visits] - node_data.sort(key=lambda datum: datum['views'], reverse=True) - - node_data = [node_dict['node'] for node_dict in node_data] - - for nid in node_data: - node = Node.load(nid) - if node is None: - continue - if node.is_public and not node.is_registration and not node.is_deleted: - if len(popular_public_projects) < max_projects_to_display: - popular_public_projects.append(node) - elif node.is_public and node.is_registration and not node.is_deleted and not node.is_retracted: - if len(popular_public_registrations) < max_projects_to_display: - popular_public_registrations.append(node) - if len(popular_public_projects) >= max_projects_to_display and len(popular_public_registrations) >= max_projects_to_display: - break - # New and Noteworthy projects are updated manually new_and_noteworthy_projects = list(Node.objects.get(guids___id=settings.NEW_AND_NOTEWORTHY_LINKS_NODE, guids___id__isnull=False).nodes_pointer) - return { 'new_and_noteworthy_projects': new_and_noteworthy_projects, 'recent_public_registrations': recent_public_registrations(), @@ -110,7 +38,6 @@ def activity(): 'popular_public_registrations': popular_public_registrations } - # Credit to https://gist.github.com/cizixs/be41bbede49a772791c08491801c396f def sizeof_fmt(num, suffix='B'): for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: diff --git a/website/routes.py b/website/routes.py index 227d68302e3..f6144b09f50 100644 --- a/website/routes.py +++ b/website/routes.py @@ -15,8 +15,6 @@ from django.conf import settings as api_settings from django.utils.encoding import smart_str from werkzeug.http import dump_cookie -from geoip2.database import Reader -from geoip2.errors import AddressNotFoundError from framework import status from framework import sentry @@ -89,11 +87,6 @@ def get_globals(): user = _get_current_user() set_status_message(user) user_institutions = [{'id': inst._id, 'name': inst.name, 'logo_path': inst.logo_path_rounded_corners} for inst in user.get_affiliated_institutions()] if user else [] - try: - location = Reader('GeoLite2-City.mmdb').city(request.remote_addr) - # TODO: replace with adequate error handling during keen removal - except (FileNotFoundError, AddressNotFoundError): - location = None if request.host_url != settings.DOMAIN: try: @@ -118,10 +111,6 @@ def get_globals(): 'user_entry_point': metrics.get_entry_point(user) if user else '', 'user_institutions': user_institutions if user else None, 'display_name': user.fullname if user else '', - 'anon': { - 'continent': (location or {}).get('continent', {}).get('code', None), - 'country': (location or {}).get('country', {}).get('iso_code', None), - }, 'use_cdn': settings.USE_CDN_FOR_CLIENT_LIBS, 'sentry_dsn_js': settings.SENTRY_DSN_JS if sentry.enabled else None, 'dev_mode': settings.DEV_MODE, @@ -150,16 +139,6 @@ def get_globals(): 'profile_url': cas.get_profile_url(), 'enable_institutions': settings.ENABLE_INSTITUTIONS, 'page_route_name': request.url_rule.endpoint.replace('__', '.'), - 'keen': { - 'public': { - 'project_id': settings.KEEN['public']['project_id'], - 'write_key': settings.KEEN['public']['write_key'], - }, - 'private': { - 'project_id': settings.KEEN['private']['project_id'], - 'write_key': settings.KEEN['private']['write_key'], - }, - }, 'institutional_landing_flag': flag_is_active(request, features.INSTITUTIONAL_LANDING_FLAG), 'maintenance': maintenance.get_maintenance(), 'recaptcha_site_key': settings.RECAPTCHA_SITE_KEY, diff --git a/website/settings/defaults.py b/website/settings/defaults.py index 70867dd901e..d891e886873 100644 --- a/website/settings/defaults.py +++ b/website/settings/defaults.py @@ -275,20 +275,6 @@ def parent_dir(path): 'node': [], } -KEEN = { - 'public': { - 'project_id': None, - 'master_key': 'changeme', - 'write_key': '', - 'read_key': '', - }, - 'private': { - 'project_id': '', - 'write_key': '', - 'read_key': '', - }, -} - SENTRY_DSN = None SENTRY_DSN_JS = None @@ -296,7 +282,6 @@ def parent_dir(path): # Most Popular and New and Noteworthy Nodes POPULAR_LINKS_NODE = None # TODO Override in local.py in production. -POPULAR_LINKS_REGISTRATIONS = None # TODO Override in local.py in production. NEW_AND_NOTEWORTHY_LINKS_NODE = None # TODO Override in local.py in production. MAX_POPULAR_PROJECTS = 10 @@ -433,7 +418,6 @@ class CeleryConfig: 'scripts.osfstorage.usage_audit', 'scripts.stuck_registration_audit', 'scripts.populate_new_and_noteworthy_projects', - 'scripts.populate_popular_projects_and_registrations', 'website.search.elastic_search', 'scripts.generate_sitemap', 'osf.management.commands.clear_expired_sessions', @@ -450,7 +434,6 @@ class CeleryConfig: 'osf.management.commands.sync_datacite_doi_metadata', 'osf.management.commands.update_institution_project_counts', 'osf.management.commands.populate_branched_from', - 'osf.management.commands.cumulative_plos_metrics', 'osf.management.commands.spam_metrics', 'osf.management.commands.daily_reporters_go', 'osf.management.commands.monthly_reporters_go', @@ -530,7 +513,6 @@ class CeleryConfig: 'website.search.search', 'website.project.tasks', 'scripts.populate_new_and_noteworthy_projects', - 'scripts.populate_popular_projects_and_registrations', 'scripts.refresh_addon_tokens', 'scripts.retract_registrations', 'scripts.embargo_registrations', @@ -555,7 +537,6 @@ class CeleryConfig: 'osf.management.commands.delete_legacy_quickfiles_nodes', 'osf.management.commands.fix_quickfiles_waterbutler_logs', 'osf.management.commands.sync_doi_metadata', - 'osf.management.commands.cumulative_plos_metrics', 'api.providers.tasks', 'osf.management.commands.daily_reporters_go', 'osf.management.commands.monthly_reporters_go', @@ -644,11 +625,6 @@ class CeleryConfig: 'schedule': crontab(minute=0, hour=7, day_of_week=6), # Saturday 2:00 a.m. 'kwargs': {'dry_run': False} }, - 'update_popular_nodes': { - 'task': 'scripts.populate_popular_projects_and_registrations', - 'schedule': crontab(minute=0, hour=7), # Daily 2:00 a.m. - 'kwargs': {'dry_run': False} - }, 'registration_schema_metrics': { 'task': 'management.commands.registration_schema_metrics', 'schedule': crontab(minute=45, hour=7, day_of_month=3), # Third day of month 2:45 a.m. @@ -744,11 +720,6 @@ class CeleryConfig: # 'schedule': crontab(minute=0, hour=11), # Daily 6 a.m # 'kwargs': {}, # }, - # 'cumulative_plos_metrics': { - # 'task': 'osf.management.commands.cumulative_plos_metrics', - # 'schedule': crontab(day_of_month=1, minute=30, hour=9), # First of the month at 4:30 a.m. - # 'kwargs': {}, - # }, # }) @@ -2089,9 +2060,6 @@ class CeleryConfig: DS_METRICS_BASE_FOLDER = None REG_METRICS_OSF_TOKEN = None REG_METRICS_BASE_FOLDER = None -PLOS_METRICS_BASE_FOLDER = None -PLOS_METRICS_INITIAL_FILE_DOWNLOAD_URL = None -PLOS_METRICS_OSF_TOKEN = None STORAGE_WARNING_THRESHOLD = .9 # percent of maximum storage used before users get a warning message STORAGE_LIMIT_PUBLIC = 50 diff --git a/website/settings/local-ci.py b/website/settings/local-ci.py index 582541525a9..8bf283b6338 100644 --- a/website/settings/local-ci.py +++ b/website/settings/local-ci.py @@ -82,23 +82,8 @@ class CeleryConfig(defaults.CeleryConfig): # if ENABLE_VARNISH isn't set in python read it from the env var and set it locals().setdefault('ENABLE_VARNISH', os.environ.get('ENABLE_VARNISH') == 'True') -KEEN = { - 'public': { - 'project_id': '123456789abcdef101112131415161718191a1b1c1d1e1f20212223242526272', - 'master_key': '123456789abcdef101112131415161718191a1b1c1d1e1f20212223242526272', - 'write_key': '123456789abcdef101112131415161718191a1b1c1d1e1f20212223242526272', - 'read_key': '123456789abcdef101112131415161718191a1b1c1d1e1f20212223242526272', - }, - 'private': { - 'project_id': '123456789abcdef101112131415161718191a1b1c1d1e1f20212223242526272', - 'write_key': '123456789abcdef101112131415161718191a1b1c1d1e1f20212223242526272', - 'read_key': '123456789abcdef101112131415161718191a1b1c1d1e1f20212223242526272', - }, -} - NEW_AND_NOTEWORTHY_LINKS_NODE = 'helloo' POPULAR_LINKS_NODE = 'hiyah' -POPULAR_LINKS_REGISTRATIONS = 'woooo' logging.getLogger('celery.app.trace').setLevel(logging.FATAL) diff --git a/website/static/js/keen.js b/website/static/js/keen.js deleted file mode 100644 index b716bc84c79..00000000000 --- a/website/static/js/keen.js +++ /dev/null @@ -1,275 +0,0 @@ -'use strict'; - - -var $ = require('jquery'); -var md5 = require('js-md5'); -var Raven = require('raven-js'); -var Cookie = require('js-cookie'); -var lodashGet = require('lodash.get'); -var keenTracking = require('keen-tracking'); -var $osf = require('js/osfHelpers'); - -var KeenTracker = (function() { - - function _nowUTC() { - var now = new Date(); - return new Date( - now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), - now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds() - ); - } - - function _createOrUpdateKeenSession() { - var expDate = new Date(); - var expiresInMinutes = 25; - expDate.setTime(expDate.getTime() + (expiresInMinutes * 60 * 1000)); - var currentSessionId = Cookie.get('keenSessionId') || keenTracking.helpers.getUniqueId(); - Cookie.set('keenSessionId', currentSessionId, {expires: expDate, path: '/'}); - return currentSessionId; - } - - function _getOrCreateKeenId() { - if (!Cookie.get('keenUserId')) { - Cookie.set('keenUserId', keenTracking.helpers.getUniqueId(), {expires: 365, path: '/'}); - } - return Cookie.get('keenUserId'); - } - - - function _defaultKeenPayload() { - _createOrUpdateKeenSession(); - - var user = window.contextVars.currentUser; - var node = window.contextVars.node; - var pageMeta = lodashGet(window, 'contextVars.analyticsMeta.pageMeta', {}); - return { - page: { - title: document.title, - url: document.URL, - meta: pageMeta, - info: {}, - }, - referrer: { - url: document.referrer, - info: {}, - }, - time: { - local: keenTracking.helpers.getDatetimeIndex(), - utc: keenTracking.helpers.getDatetimeIndex(_nowUTC()), - }, - node: { - id: lodashGet(node, 'id'), - title: lodashGet(node, 'title'), - type: lodashGet(node, 'category'), - tags: lodashGet(node, 'tags'), - }, - anon: { - id: md5(Cookie.get('keenSessionId')), - continent: user.anon.continent, - country: user.anon.country, - }, - meta: { - epoch: 1, // version of pageview event schema - }, - keen: { - addons: [ - { - name: 'keen:url_parser', - input: { - url: 'page.url', - }, - output: 'page.info', - }, - { - name: 'keen:url_parser', - input: { - url: 'referrer.url', - }, - output: 'referrer.info', - }, - { - name: 'keen:referrer_parser', - input: { - referrer_url: 'referrer.url', - page_url: 'page.url', - }, - output: 'referrer.info', - }, - ] - }, - }; - } // end _defaultKeenPayload - - function _trackCustomEvent(client, collection, eventData) { - if (client === null) { - return; - } - client.recordEvent(collection, eventData, function (err) { - if (err) { - // If google analytics is inaccessible keen will throw errors - var adBlockError = document.getElementsByTagName('iframe').item(0) === null; - var uselessError = 'An error occurred!' === err; - if(!adBlockError || !uselessError) { - Raven.captureMessage('Error sending Keen data to ' + collection + ': <' + err + '>', { - extra: {payload: eventData} - }); - } - } - }); - } - - function _pageIsPublic() { - return Boolean( - lodashGet(window, 'contextVars.node.isPublic', false) && - lodashGet(window, 'contextVars.analyticsMeta.pageMeta.public', false) - ); - } - - function _getActionLabels() { - const actionLabelMap = { - 'web': true, - 'view': Boolean(lodashGet(window, 'contextVars.analyticsMeta.itemGuid')), - 'search': Boolean(lodashGet(window, 'contextVars.analyticsMeta.searchProviderId')), - }; - return ( - Object.keys(actionLabelMap) - .filter(label => Boolean(actionLabelMap[label])) - ); - } - - function _logPageview() { - const url = new URL('/_/metrics/events/counted_usage/', window.contextVars.apiV2Domain); - const sessionId = _createOrUpdateKeenSession(); - const data = { - type: 'counted-usage', - attributes: { - client_session_id: sessionId ? md5(sessionId) : null, - provider_id: lodashGet(window, 'contextVars.analyticsMeta.searchProviderId'), - item_guid: lodashGet(window, 'contextVars.analyticsMeta.itemGuid'), - item_public: _pageIsPublic(), - action_labels: _getActionLabels(), - pageview_info: { - referer_url: document.referrer, - page_url: document.URL, - page_title: document.title, - route_name: lodashGet(window, 'contextVars.analyticsMeta.pageMeta.routeName'), - }, - }, - }; - - $osf.ajaxJSON('POST', url.toString(), { - isCors: true, - data: {data}, - }); - } - - function KeenTracker() { - if (instance) { - throw new Error('Cannot instantiate another KeenTracker instance.'); - } else { - var self = this; - - self._publicClient = null; - self._privateClient = null; - - self.init = function _initKeentracker(params) { - var self = this; - - if (params === undefined) { - return self; - } - - self._publicClient = new keenTracking({ - projectId: params.public.projectId, - writeKey: params.public.writeKey, - }); - self._publicClient.extendEvents(_defaultPublicKeenPayload); - - self._privateClient = new keenTracking({ - projectId: params.private.projectId, - writeKey: params.private.writeKey, - }); - self._privateClient.extendEvents(_defaultPrivateKeenPayload); - - return self; - }; - - var _defaultPublicKeenPayload = function() { return _defaultKeenPayload(); }; - var _defaultPrivateKeenPayload = function() { - var payload = _defaultKeenPayload(); - var user = window.contextVars.currentUser; - payload.visitor = { - id: _getOrCreateKeenId(), - session: Cookie.get('keenSessionId'), - returning: Boolean(Cookie.get('keenUserId')), - }; - payload.tech = { - browser: keenTracking.helpers.getBrowserProfile(), - ua: '${keen.user_agent}', - ip: '${keen.ip}', - info: {}, - }; - payload.user = { - id: user.id, - entry_point: user.entryPoint, - institutions: user.institutions, - locale: user.locale, - timezone: user.timezone, - }; - payload.keen.addons.push({ - name: 'keen:ip_to_geo', - input: { - ip: 'tech.ip', - remove_ip_property: true - }, - output: 'geo', - }); - payload.keen.addons.push({ - name: 'keen:ua_parser', - input: { - ua_string: 'tech.ua' - }, - output: 'tech.info', - }); - - return payload; - }; - - self.trackPageView = function () { - _logPageview(); - - var self = this; - var guid; - if (_pageIsPublic()) { - guid = lodashGet(window, 'contextVars.node.id', null); - if (guid) { - var partitioned_collection = 'pageviews-' + guid.charAt(0); - self.trackPublicEvent(partitioned_collection, {}); - } - } - self.trackPrivateEvent('pageviews', {}); - }; - - self.trackPrivateEvent = function(collection, event) { - return _trackCustomEvent(self._privateClient, collection, event); - }; - - self.trackPublicEvent = function(collection, event) { - return _trackCustomEvent(self._publicClient, collection, event); - }; - } - } - - var instance = null; - return { - getInstance: function() { - if (!instance) { - instance = new KeenTracker(); - instance.init(window.contextVars.keen); - } - return instance; - } - }; -})(); - -module.exports = KeenTracker; diff --git a/website/static/js/metrics.js b/website/static/js/metrics.js new file mode 100644 index 00000000000..a9f86eb811f --- /dev/null +++ b/website/static/js/metrics.js @@ -0,0 +1,106 @@ +'use strict'; + + +var md5 = require('js-md5'); +var Cookie = require('js-cookie'); +var lodashGet = require('lodash.get'); +var $osf = require('js/osfHelpers'); + +var MetricsTracker = (function() { + + // approach taken from: https://github.com/keen/keen-tracking.js/blob/master/lib/helpers/getUniqueId.js + // which originally came from: http://stackoverflow.com/a/2117523/2511985 + function getUniqueId() { + if (typeof window.crypto !== 'undefined' && window.crypto.getRandomValues) { + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ window.crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); + } else { + let str = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; + return str.replace(/[xy]/g, function(c) { + let r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + } + } + + function _createOrUpdateMetricsSession() { + var expDate = new Date(); + var expiresInMinutes = 25; + expDate.setTime(expDate.getTime() + (expiresInMinutes * 60 * 1000)); + var currentSessionId = Cookie.get('osfMetricsSessionId') || getUniqueId(); + Cookie.set('osfMetricsSessionId', currentSessionId, {expires: expDate, path: '/'}); + return currentSessionId; + } + + + function _pageIsPublic() { + return Boolean( + lodashGet(window, 'contextVars.node.isPublic', false) && + lodashGet(window, 'contextVars.analyticsMeta.pageMeta.public', false) + ); + } + + function _getActionLabels() { + const actionLabelMap = { + 'web': true, + 'view': Boolean(lodashGet(window, 'contextVars.analyticsMeta.itemGuid')), + 'search': Boolean(lodashGet(window, 'contextVars.analyticsMeta.searchProviderId')), + }; + return ( + Object.keys(actionLabelMap) + .filter(label => Boolean(actionLabelMap[label])) + ); + } + + function _logPageview() { + const url = new URL('/_/metrics/events/counted_usage/', window.contextVars.apiV2Domain); + const sessionId = _createOrUpdateMetricsSession(); + const data = { + type: 'counted-usage', + attributes: { + client_session_id: sessionId ? md5(sessionId) : null, + provider_id: lodashGet(window, 'contextVars.analyticsMeta.searchProviderId'), + item_guid: lodashGet(window, 'contextVars.analyticsMeta.itemGuid'), + item_public: _pageIsPublic(), + action_labels: _getActionLabels(), + pageview_info: { + referer_url: document.referrer, + page_url: document.URL, + page_title: document.title, + route_name: lodashGet(window, 'contextVars.analyticsMeta.pageMeta.routeName'), + }, + }, + }; + + $osf.ajaxJSON('POST', url.toString(), { + isCors: true, + data: {data}, + }); + } + + function MetricsTracker() { + if (instance) { + throw new Error('Cannot instantiate another MetricsTracker instance.'); + } else { + var self = this; + + self.trackPageView = function () { + _logPageview(); + }; + + } + } + + var instance = null; + return { + getInstance: function() { + if (!instance) { + instance = new MetricsTracker(); + } + return instance; + } + }; +})(); + +module.exports = MetricsTracker; diff --git a/website/static/js/pages/base-page.js b/website/static/js/pages/base-page.js index 079e0f9a74e..877239bd271 100644 --- a/website/static/js/pages/base-page.js +++ b/website/static/js/pages/base-page.js @@ -19,7 +19,7 @@ var NavbarControl = require('js/navbarControl'); var AlertManager = require('js/alertsManager'); var Raven = require('raven-js'); var moment = require('moment'); -var KeenTracker = require('js/keen'); +var MetricsTracker = require('js/metrics'); var DevModeControls = require('js/devModeControls'); var bootbox = require('bootbox'); @@ -252,9 +252,9 @@ $(function() { } } - //Don't track PhantomJS visits with KeenIO + //Don't track PhantomJS visits with metrics if (!(/PhantomJS/.test(navigator.userAgent))){ - KeenTracker.getInstance().trackPageView(); + MetricsTracker.getInstance().trackPageView(); } confirmEmails(window.contextVars.currentUser.emailsToAdd); diff --git a/website/templates/base.mako b/website/templates/base.mako index fceb1842931..2561d2da961 100644 --- a/website/templates/base.mako +++ b/website/templates/base.mako @@ -258,7 +258,6 @@ entryPoint: ${ user_entry_point | sjson, n }, institutions: ${ user_institutions | sjson, n}, emailsToAdd: ${ user_email_verifications | sjson, n }, - anon: ${ anon | sjson, n }, }, maintenance: ${ maintenance | sjson, n}, analyticsMeta: { @@ -271,23 +270,6 @@ }); - % if keen['public']['project_id']: - - % endif - % if settings.DATACITE_TRACKER_REPO_ID: % endif - ${self.javascript_bottom()} diff --git a/yarn.lock b/yarn.lock index 0b6b7f54a21..7dcf3023814 100644 --- a/yarn.lock +++ b/yarn.lock @@ -399,7 +399,7 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -bluebird@^3.2.1, bluebird@^3.5.1: +bluebird@^3.5.1: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -831,11 +831,6 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -component-emitter@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -2161,11 +2156,6 @@ js-base64@^2.1.9: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== -js-cookie@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.1.0.tgz#479c20d0a0bb6cab81491f917788cd025d6452f0" - integrity sha1-R5wg0KC7bKuBSR+Rd4jNAl1kUvA= - js-cookie@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414" @@ -2241,28 +2231,6 @@ jstimezonedetect@^1.0.6: resolved "https://registry.yarnpkg.com/jstimezonedetect/-/jstimezonedetect-1.0.7.tgz#54bc13ff9960a6510288665cc9596244e21bd29d" integrity sha512-ARADHortktl9IZ1tr4GHwGPIAzgz3mLNCbR/YjWtRtc/O0o634O3NeFlpLjv95EvuDA5dc8z6yfgbS8nUc4zcQ== -keen-analysis@^1.1.0: - version "1.3.3" - resolved "https://registry.yarnpkg.com/keen-analysis/-/keen-analysis-1.3.3.tgz#9a10f5b36cea0e16497f53cbca7695ea51b978fe" - integrity sha512-sAedN2UYXA6j4EXeZgg37hxu9XFp+g7QtUgDeXVJb9Cs++dgRwoj/6Li+wNiV7Q667T8b2phHkCFvm8o8uueGw== - dependencies: - bluebird "^3.2.1" - keen-core "^0.2.0" - -keen-core@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/keen-core/-/keen-core-0.0.3.tgz#83f14516147c1a4fe9b459b0c0adf392b3c8c336" - integrity sha1-g/FFFhR8Gk/ptFmwwK3zkrPIwzY= - dependencies: - component-emitter "^1.2.0" - -keen-core@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/keen-core/-/keen-core-0.2.0.tgz#5910f9309b6ef2901cd3493bab5a1837b00a9b5b" - integrity sha1-WRD5MJtu8pAc00k7q1oYN7AKm1s= - dependencies: - component-emitter "^1.2.0" - keen-dataviz@^1.0.2: version "1.2.1" resolved "https://registry.yarnpkg.com/keen-dataviz/-/keen-dataviz-1.2.1.tgz#6f7595a2051652e1bece2ce2296e47ffa5d79dd9" @@ -2271,15 +2239,6 @@ keen-dataviz@^1.0.2: c3 "^0.4.18" d3 "^3.5.17" -keen-tracking@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/keen-tracking/-/keen-tracking-1.0.3.tgz#97e86f154d9b00ee2f3230faa2b1f367b6e7c180" - integrity sha1-l+hvFU2bAO4vMjD6orHzZ7bnwYA= - dependencies: - component-emitter "^1.2.0" - js-cookie "2.1.0" - keen-core "0.0.3" - keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" From af28216ff8516a6d1265fa6c484f4506645fa2b9 Mon Sep 17 00:00:00 2001 From: ihorsokhanexoft Date: Fri, 11 Apr 2025 15:52:50 +0300 Subject: [PATCH 5/9] [ENG-7799] Update authors' names format in metadata (#11091) ## Purpose In metadata we return authors' names as full names. We should use this format instead: "{family name}, {given name}" Also added givenName and familyName in JSON-LD schema ## Changes Updated function that generates names, added `given_name` attribute when serialize contributors in mako templates ## Ticket https://openscience.atlassian.net/browse/ENG-7799 --- osf/metadata/serializers/google_dataset_json_ld.py | 6 ++++-- website/profile/utils.py | 1 + website/templates/base.mako | 8 ++++++-- website/templates/project/project_base.mako | 8 ++++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/osf/metadata/serializers/google_dataset_json_ld.py b/osf/metadata/serializers/google_dataset_json_ld.py index 04deebede00..896e80acc7c 100644 --- a/osf/metadata/serializers/google_dataset_json_ld.py +++ b/osf/metadata/serializers/google_dataset_json_ld.py @@ -76,10 +76,12 @@ def metadata_as_dict(self) -> dict: def format_creators(basket): creator_data = [] - for creator_iri in basket[DCTERMS.creator]: + for creator in basket.focus.dbmodel.contributors.all(): creator_data.append({ '@type': 'Person', - 'name': next(basket[creator_iri:FOAF.name]), + 'name': creator.fullname, + 'givenName': creator.given_name, + 'familyName': creator.family_name }) return creator_data diff --git a/website/profile/utils.py b/website/profile/utils.py index 5b1e22e8916..468fa42fd4a 100644 --- a/website/profile/utils.py +++ b/website/profile/utils.py @@ -31,6 +31,7 @@ def serialize_user(user, node=None, admin=False, full=False, is_profile=False, i 'id': str(user._id), 'registered': user.is_registered, 'surname': user.family_name, + 'given_name': user.given_name, 'fullname': fullname, 'shortname': fullname if len(fullname) < 50 else fullname[:23] + '...' + fullname[-23:], 'profile_image_url': user.profile_image_url(size=settings.PROFILE_IMAGE_MEDIUM), diff --git a/website/templates/base.mako b/website/templates/base.mako index 2561d2da961..642f8ffa03c 100644 --- a/website/templates/base.mako +++ b/website/templates/base.mako @@ -75,8 +75,8 @@ - %for author in self.authors_meta()[:10]: - + %for author, creator in list(zip(self.authors_meta()[:10], self.creator_meta()[:10])): + %endfor %for tag in self.keywords_meta()[:10]: @@ -329,6 +329,10 @@ ### The list of project contributors ### +<%def name="creator_meta()"> + ### The list of project creators ### + + <%def name="datemodified_meta()"> ### The project last modified date. diff --git a/website/templates/project/project_base.mako b/website/templates/project/project_base.mako index 6dcab9e4175..68ff9354038 100644 --- a/website/templates/project/project_base.mako +++ b/website/templates/project/project_base.mako @@ -69,6 +69,14 @@ <%def name="authors_meta()"> + %if node['contributors'] and not node['anonymous']: + <% + return [f'{contrib['surname']}, {contrib['given_name']}'for contrib in node['contributors'] if isinstance(contrib, dict)] + %> + %endif + + +<%def name="creator_meta()"> %if node['contributors'] and not node['anonymous']: <% return [contrib['fullname'] for contrib in node['contributors'] if isinstance(contrib, dict)] From 7814453bb66aa9349231233b87ed7632d9025866 Mon Sep 17 00:00:00 2001 From: antkryt Date: Fri, 11 Apr 2025 15:57:57 +0300 Subject: [PATCH 6/9] [ENG-7750] Preprint creator cannot remove bibliographic citation status (#11087) ## Purpose fix preprint creator cannot remove bibliographic citation status ## Changes - move restriction logic to serializer field validator to validate only permission field during any updates: PATCH, PUT - add more tests ## Ticket https://openscience.atlassian.net/browse/ENG-7750 --- api/preprints/serializers.py | 15 +++++ api/preprints/views.py | 15 +---- .../test_preprint_contributors_detail.py | 49 ++++++++++++++ osf_tests/test_draft_registration.py | 27 ++++++++ tests/test_preprints.py | 66 ------------------- 5 files changed, 94 insertions(+), 78 deletions(-) diff --git a/api/preprints/serializers.py b/api/preprints/serializers.py index 399d7bca343..2e802a438a0 100644 --- a/api/preprints/serializers.py +++ b/api/preprints/serializers.py @@ -49,6 +49,7 @@ NodeLicense, ) from osf.utils import permissions as osf_permissions +from osf.utils.workflows import DefaultStates class PrimaryFileRelationshipField(RelationshipField): @@ -625,6 +626,20 @@ class PreprintContributorDetailSerializer(NodeContributorDetailSerializer, Prepr id = IDField(required=True, source='_id') index = ser.IntegerField(required=False, read_only=False, source='_order') + def validate_permission(self, value): + preprint = self.context.get('resource') + user = self.context.get('user') + if ( + user # if user is None then probably we're trying to make bulk update and this validation is not relevant + and preprint.machine_state == DefaultStates.INITIAL.value + and preprint.creator_id == user.id + ): + raise ValidationError( + 'You cannot change your permission setting at this time. ' + 'Have another admin contributor edit your permission after you’ve submitted your preprint', + ) + return value + class PreprintStorageProviderSerializer(NodeStorageProviderSerializer): node = HideIfPreprint(ser.CharField(source='node_id', read_only=True)) diff --git a/api/preprints/views.py b/api/preprints/views.py index 4b7d9b6c7f0..af1cb3af974 100644 --- a/api/preprints/views.py +++ b/api/preprints/views.py @@ -77,10 +77,10 @@ class PreprintOldVersionsImmutableMixin: @staticmethod def is_edit_allowed(preprint): - if preprint.is_latest_version or preprint.machine_state == 'initial': + if preprint.is_latest_version or preprint.machine_state == DefaultStates.INITIAL.value: return True if preprint.provider.reviews_workflow == Workflows.PRE_MODERATION.value: - if preprint.machine_state == 'pending' or preprint.machine_state == 'rejected': + if preprint.machine_state == DefaultStates.PENDING.value or preprint.machine_state == DefaultStates.REJECTED.value: return True return False @@ -535,19 +535,10 @@ def get_object(self): def get_serializer_context(self): context = JSONAPIBaseView.get_serializer_context(self) context['resource'] = self.get_preprint() + context['user'] = self.get_user() context['default_email'] = 'preprint' return context - def patch(self, *args, **kwargs): - preprint = self.get_resource() - user = self.get_user() - if preprint.machine_state == DefaultStates.INITIAL.value and preprint.creator_id == user.id: - raise ValidationError( - 'You cannot change your permission setting at this time. ' - 'Have another admin contributor edit your permission after you’ve submitted your preprint', - ) - return super().patch(*args, **kwargs) - def perform_destroy(self, instance): preprint = self.get_resource() auth = get_user_auth(self.request) diff --git a/api_tests/preprints/views/test_preprint_contributors_detail.py b/api_tests/preprints/views/test_preprint_contributors_detail.py index 170c75a99f8..16c05911d23 100644 --- a/api_tests/preprints/views/test_preprint_contributors_detail.py +++ b/api_tests/preprints/views/test_preprint_contributors_detail.py @@ -1022,6 +1022,33 @@ def test_patch_permission_only(self, app, user, preprint): assert preprint.get_permissions(user_read_contrib) == [permissions.READ] assert not preprint.get_visible(user_read_contrib) + def test_patch_admin_self_initial_preprint(self, app, user, preprint): + new_version = PreprintFactory.create_version( + create_from=preprint, + creator=user, + final_machine_state='initial', + is_published=False, + set_doi=False + ) + + contrib_id = f'{new_version._id}-{user._id}' + data = { + 'data': { + 'id': contrib_id, + 'type': 'contributors', + 'attributes': { + 'permission': permissions.WRITE, + 'bibliographic': True + } + } + } + url = f'/{API_BASE}preprints/{new_version._id}/contributors/{user._id}/' + res = app.patch_json_api(url, data, auth=user.auth, expect_errors=True) + assert res.status_code == 400 + + new_version.reload() + assert permissions.ADMIN in new_version.get_permissions(user) + @pytest.mark.django_db class TestPreprintContributorDelete: @@ -1190,6 +1217,28 @@ def test_remove_self_contributor_not_unique_admin( preprint.reload() assert user not in preprint.contributors + def test_remove_self_creator_not_unique_admin_initial_preprint( + self, app, user, user_write_contrib, preprint): + new_version = PreprintFactory.create_version( + create_from=preprint, + creator=user, + final_machine_state='initial', + is_published=False, + set_doi=False + ) + + new_version.add_permission( + user_write_contrib, + permissions.ADMIN, + save=True + ) + assert len(new_version.contributors) == 2 + assert new_version.creator_id == user.id + + url = f'/{API_BASE}preprints/{new_version._id}/contributors/{user._id}/' + res = app.delete(url, auth=user.auth, expect_errors=True) + assert res.status_code == 400 + # @assert_logs(PreprintLog.CONTRIB_REMOVED, 'preprint') def test_can_remove_self_as_contributor_not_unique_admin( self, app, user_write_contrib, preprint, url_user_write_contrib): diff --git a/osf_tests/test_draft_registration.py b/osf_tests/test_draft_registration.py index 225719efdd4..f7beb3ceae8 100644 --- a/osf_tests/test_draft_registration.py +++ b/osf_tests/test_draft_registration.py @@ -571,6 +571,33 @@ def test_update_contributor(self, draft_registration, auth): assert draft_registration.get_permissions(new_contrib) == [READ] assert draft_registration.get_visible(new_contrib) is False + def test_remove_admin_permission(self, draft_registration, auth): + admin_user = auth.user + assert len(draft_registration.contributors) == 1 + assert draft_registration.has_permission(admin_user, ADMIN) is True + + with pytest.raises(DraftRegistrationStateError) as exc_info: + draft_registration.update_contributor( + admin_user, + WRITE, + draft_registration.get_visible(admin_user), + auth=auth + ) + assert str(exc_info.value) == f"{admin_user.fullname} is the only admin." + + # user should be able to remove their own admin permission if there're 2+ admin contributors + new_contrib = factories.AuthUserFactory() + draft_registration.add_contributor(new_contrib, permissions=ADMIN, auth=auth) + assert draft_registration.has_permission(new_contrib, ADMIN) is True + + draft_registration.update_contributor( + admin_user, + WRITE, + draft_registration.get_visible(admin_user), + auth=auth, + ) + assert draft_registration.has_permission(admin_user, ADMIN) is False + def test_update_contributor_non_admin_raises_error(self, draft_registration, auth): non_admin = factories.AuthUserFactory() draft_registration.add_contributor( diff --git a/tests/test_preprints.py b/tests/test_preprints.py index c975203fdde..a213c961659 100644 --- a/tests/test_preprints.py +++ b/tests/test_preprints.py @@ -2739,69 +2739,3 @@ def test_ember_redirect_to_versioned_guid(self): location_with_no_guid = res.location.replace(guid_with_version, '') # check if location has not wrong format https://osf.io/preprints/socarxiv/3rhyz/3rhyz_v1 assert location_with_no_guid == location_with_no_guid.replace(guid_with_no_version, '') - -@pytest.mark.django_db -class TestPreprintCreatorStateChangings: - - @pytest.fixture() - def creator(self): - return AuthUserFactory() - - @pytest.fixture() - def unpublished_preprint_pre_mod(self): - return PreprintFactory(reviews_workflow='pre-moderation', is_published=False) - - def test_preprint_initial_creator_removal(self, creator, unpublished_preprint_pre_mod): - new_version = PreprintFactory.create_version( - create_from=unpublished_preprint_pre_mod, - creator=creator, - final_machine_state='initial', - is_published=False, - set_doi=False - ) - request = RequestFactory().delete('/fake_path') - request.user = creator - request.query_params = {} - request.parser_context = { - 'kwargs': { - 'preprint_id': new_version._id, - 'user_id': creator._id - }, - } - view = PreprintContributorDetail() - view = setup_view(view, request, preprint_id=new_version._id, user_id=creator._id) - try: - view.perform_destroy(request) - except Exception as error: - assert error.args[0] == ('You cannot delete yourself at this time. ' - 'Have another admin contributor do that after you’ve submitted your preprint') - else: - assert False - - def test_preprint_initial_creator_update(self, creator, unpublished_preprint_pre_mod): - new_version = PreprintFactory.create_version( - create_from=unpublished_preprint_pre_mod, - creator=creator, - final_machine_state='initial', - is_published=False, - set_doi=False - ) - request = RequestFactory().patch('/fake_path') - request.user = creator - request.query_params = {} - request.parser_context = { - 'kwargs': { - 'preprint_id': new_version._id, - 'user_id': creator._id - }, - } - view = PreprintContributorDetail() - view = setup_view(view, request, preprint_id=new_version._id, user_id=creator._id) - try: - view.patch(request) - except Exception as error: - assert error.args[0] == ('You cannot change your permission setting at this time. ' - 'Have another admin contributor edit your permission ' - 'after you’ve submitted your preprint') - else: - assert False From c43cc32c08efff42d301a4a7af7a6205e52fba07 Mon Sep 17 00:00:00 2001 From: antkryt Date: Fri, 11 Apr 2025 16:04:46 +0300 Subject: [PATCH 7/9] [ENG-7289] Fix Search Index Discrepancy in Collection Facets (#11085) ## Purpose Update SHARE index when collection submission is created ## Changes - add update_share() call - minor collection submission transitions refactor to remove redundant transitions ## QA Notes This PR won't fix collection submission indexes that were not created, but will prevent such situation in the future ## Ticket https://openscience.atlassian.net/browse/ENG-7289 --- api_tests/collections/test_views.py | 13 +++++++++++++ .../commands/sync_collection_provider_indices.py | 2 +- osf/models/collection_submission.py | 12 ++++++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/api_tests/collections/test_views.py b/api_tests/collections/test_views.py index 3f7d414663f..025060cc4ee 100644 --- a/api_tests/collections/test_views.py +++ b/api_tests/collections/test_views.py @@ -21,6 +21,7 @@ from osf.utils.permissions import ADMIN, WRITE, READ from website.project.signals import contributor_removed from api_tests.utils import disconnected_from_listeners +from api_tests.share._utils import mock_update_share from website.views import find_bookmark_collection @@ -4340,6 +4341,18 @@ def test_with_permissions(self, app, collection_with_three_collection_submission assert len(res.json['data']) == 0 assert res.status_code == 200 + def test_update_share_called(self, app, collection_with_zero_collection_submission, user_one, project_four, url, payload, subject_one): + collection_with_zero_collection_submission.is_public = True + collection_with_zero_collection_submission.save() + + with mock_update_share() as _shmock: + res = app.post_json_api( + url.format(collection_with_zero_collection_submission._id), + payload(guid=project_four._id, subjects=[[subject_one._id]]), + auth=user_one.auth) + assert res.status_code == 201 + _shmock.assert_called() + def test_filters(self, app, collection_with_one_collection_submission, collection_with_three_collection_submission, project_two, project_four, user_one, subject_one, url, payload): res = app.get(f'{url.format(collection_with_three_collection_submission._id)}?filter[id]={project_two._id}', auth=user_one.auth) assert res.status_code == 200 diff --git a/osf/management/commands/sync_collection_provider_indices.py b/osf/management/commands/sync_collection_provider_indices.py index 33e1eb015f4..54adaa6677a 100644 --- a/osf/management/commands/sync_collection_provider_indices.py +++ b/osf/management/commands/sync_collection_provider_indices.py @@ -26,7 +26,7 @@ def sync_collection_provider_indices(cp_ids=None, only_remove=False): submission.remove_from_index() remove_ct += 1 elif not only_remove: - submission.update_index() + submission.update_search() add_ct += 1 logger.info(f'{remove_ct} submissions removed from {prov._id}') logger.info(f'{add_ct} submissions synced to {prov._id}') diff --git a/osf/models/collection_submission.py b/osf/models/collection_submission.py index d8ef9193fb0..893533d85d1 100644 --- a/osf/models/collection_submission.py +++ b/osf/models/collection_submission.py @@ -2,6 +2,7 @@ from django.db import models from django.utils.functional import cached_property +from framework import sentry from framework.exceptions import PermissionsError from .base import BaseModel @@ -382,13 +383,19 @@ def absolute_api_v2_url(self): path = f'/collections/{self.collection._id}/collection_submissions/{self.guid._id}/' return api_v2_url(path) - def update_index(self): + def update_search(self): if self.collection.is_public: + from api.share.utils import update_share from website.search.search import update_collected_metadata + + # It will automatically determine if a referent is part of the collection + update_share(self.guid.referent) + try: update_collected_metadata(self.guid._id, collection_id=self.collection.id) except SearchUnavailableError as e: logger.exception(e) + sentry.log_exception(e) def remove_from_index(self): from website.search.search import update_collected_metadata @@ -396,10 +403,11 @@ def remove_from_index(self): update_collected_metadata(self.guid._id, collection_id=self.collection.id, op='delete') except SearchUnavailableError as e: logger.exception(e) + sentry.log_exception(e) def save(self, *args, **kwargs): ret = super().save(*args, **kwargs) - self.update_index() + self.update_search() return ret From 7571fc60491bbf3fdde00d78d07241f26465ae0e Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Fri, 11 Apr 2025 08:35:26 -0400 Subject: [PATCH 8/9] remove registration_metadata and registered_meta --- api/collections/views.py | 1 - api/nodes/serializers.py | 17 - api/nodes/views.py | 1 - api/registrations/serializers.py | 31 +- api/registrations/views.py | 1 - api/search/views.py | 1 - api/sparse/serializers.py | 1 - .../views/test_registration_list.py | 72 +- .../views/test_view_only_query_parameter.py | 28 +- .../views/test_withdrawn_registrations.py | 1 - osf/management/commands/change_node_region.py | 1 - .../commands/export_user_account.py | 1 - .../commands/fix_registration_file_domains.py | 3 - .../migrate_registration_responses.py | 9 - ...e_abstractnode_registered_meta_and_more.py | 21 + osf/models/mixins.py | 32 - osf/models/node.py | 3 - osf/models/registrations.py | 22 - osf/utils/registrations.py | 333 --- osf_tests/factories.py | 1 - .../test_migration_registration_responses.py | 2084 ----------------- osf_tests/test_archiver.py | 8 +- osf_tests/test_draft_registration.py | 34 - osf_tests/test_node.py | 3 - osf_tests/test_registrations.py | 25 - website/archiver/utils.py | 1 - website/project/views/drafts.py | 106 +- website/project/views/node.py | 2 - website/routes.py | 19 - 29 files changed, 103 insertions(+), 2759 deletions(-) create mode 100644 osf/migrations/0029_remove_abstractnode_registered_meta_and_more.py delete mode 100644 osf_tests/management_commands/test_migration_registration_responses.py diff --git a/api/collections/views.py b/api/collections/views.py index 53d29fb9113..5935194e5b5 100644 --- a/api/collections/views.py +++ b/api/collections/views.py @@ -626,7 +626,6 @@ class LinkedRegistrationsList(BaseLinkedList, CollectionMixin): pending_withdrawal boolean is this registration pending withdrawal? pending_withdrawal_approval boolean is this registration pending approval? pending_embargo_approval boolean is the associated Embargo awaiting approval by project admins? - registered_meta dictionary registration supplementary information registration_supplement string registration template ##Links diff --git a/api/nodes/serializers.py b/api/nodes/serializers.py index 879eff63d49..aaf6c5daa1c 100644 --- a/api/nodes/serializers.py +++ b/api/nodes/serializers.py @@ -1488,7 +1488,6 @@ class DraftRegistrationLegacySerializer(JSONAPISerializer): id = IDField(source='_id', read_only=True) type = TypeField() # Will be eventually deprecated in favor of registration_responses - registration_metadata = ser.DictField(required=False) registration_responses = ser.DictField(required=False) datetime_initiated = VersionedDateTimeField(read_only=True) datetime_updated = VersionedDateTimeField(read_only=True) @@ -1547,35 +1546,21 @@ def update_registration_responses(self, draft, registration_responses, required_ draft.update_registration_responses(registration_responses) draft.save() - def enforce_metadata_or_registration_responses(self, metadata=None, registration_responses=None): - if metadata and registration_responses: - raise exceptions.ValidationError( - 'You cannot include both `registration_metadata` and `registration_responses` in your request. Please use' + - ' `registration_responses` as `registration_metadata` will be deprecated in the future.', - ) - def get_node(self, validated_data=None): return self.context['view'].get_node() def create(self, validated_data): initiator = get_user_auth(self.context['request']).user node = self.get_node(validated_data) - # Old workflow - deeply nested - metadata = validated_data.pop('registration_metadata', None) registration_responses = validated_data.pop('registration_responses', None) schema = validated_data.pop('registration_schema') provider = validated_data.pop('provider', None) - self.enforce_metadata_or_registration_responses(metadata, registration_responses) - try: draft = DraftRegistration.create_from_node(node=node, user=initiator, schema=schema, provider=provider) except ValidationError as e: raise exceptions.ValidationError(e.message) - if metadata: - self.update_metadata(draft, metadata) - if registration_responses: self.update_registration_responses(draft, registration_responses) @@ -1617,8 +1602,6 @@ def update(self, draft, validated_data): metadata = validated_data.pop('registration_metadata', None) registration_responses = validated_data.pop('registration_responses', None) - self.enforce_metadata_or_registration_responses(metadata, registration_responses) - if metadata: self.update_metadata(draft, metadata) if registration_responses: diff --git a/api/nodes/views.py b/api/nodes/views.py index 7bc3ad929da..3048099ae91 100644 --- a/api/nodes/views.py +++ b/api/nodes/views.py @@ -2109,7 +2109,6 @@ class NodeLinkedRegistrationsList(BaseLinkedList, NodeMixin): pending_withdrawal boolean is this registration pending withdrawal? pending_withdrawal_approval boolean is this registration pending approval? pending_embargo_approval boolean is the associated Embargo awaiting approval by project admins? - registered_meta dictionary registration supplementary information registration_supplement string registration template ##Links diff --git a/api/registrations/serializers.py b/api/registrations/serializers.py index 0e3542131fe..f97044f107e 100644 --- a/api/registrations/serializers.py +++ b/api/registrations/serializers.py @@ -1,5 +1,4 @@ import pytz -import json from website.archiver.utils import normalize_unicode_filenames from packaging.version import Version @@ -36,7 +35,6 @@ from framework.auth.core import Auth from osf.exceptions import NodeStateError from osf.models import Node -from osf.utils.registrations import strip_registered_meta_comments from osf.utils.workflows import ApprovalStates class RegistrationSerializer(NodeSerializer): @@ -62,7 +60,6 @@ class RegistrationSerializer(NodeSerializer): 'original_response', 'provider', 'provider_specific_metadata', - 'registered_meta', 'registration_responses', 'registration_schema', 'registration_supplement', @@ -185,12 +182,6 @@ class RegistrationSerializer(NodeSerializer): has_supplements = HideIfWithdrawal(ser.BooleanField(read_only=True, required=False)) registration_supplement = ser.SerializerMethodField() - # Will be deprecated in favor of registration_responses - registered_meta = HideIfWithdrawal( - ser.SerializerMethodField( - help_text='A dictionary with supplemental registration questions and responses.', - ), - ) registration_responses = HideIfWithdrawal( ser.SerializerMethodField( help_text='A dictionary with supplemental registration questions and responses.', @@ -510,17 +501,6 @@ def get_has_project(self, obj): def get_absolute_url(self, obj): return obj.get_absolute_url() - def get_registered_meta(self, obj): - if obj.registered_meta: - meta_values = self.anonymize_registered_meta(obj) - try: - return json.loads(meta_values) - except TypeError: - return meta_values - except ValueError: - return meta_values - return None - def get_registration_responses(self, obj): latest_approved_response = obj.root.schema_responses.filter( reviews_state=ApprovalStates.APPROVED.db_name, @@ -572,15 +552,6 @@ def get_latest_response_id(self, obj): return latest_approved._id return None - def anonymize_registered_meta(self, obj): - """ - Looks at every question on every page of the schema, for any titles - that have a contributor-input block type. If present, deletes that question's response - from meta_values. - """ - cleaned_registered_meta = strip_registered_meta_comments(list(obj.registered_meta.values())[0]) - return self.anonymize_fields(obj, cleaned_registered_meta) - def anonymize_registration_responses(self, obj): """ For any questions that have a `contributor-input` block type, delete @@ -594,7 +565,7 @@ def anonymize_registration_responses(self, obj): def anonymize_fields(self, obj, data): """ Consolidates logic to anonymize fields with contributor information - on both registered_meta and registration_responses + on registration_responses """ if is_anonymized(self.context['request']): anonymous_registration_response_keys = obj.get_contributor_registration_response_keys() diff --git a/api/registrations/views.py b/api/registrations/views.py index 29305d23ce2..8eb8f03e7d4 100644 --- a/api/registrations/views.py +++ b/api/registrations/views.py @@ -800,7 +800,6 @@ class RegistrationLinkedRegistrationsList(NodeLinkedRegistrationsList, Registrat pending_withdrawal boolean is this registration pending withdrawal? pending_withdrawal_approval boolean is this registration pending approval? pending_embargo_approval boolean is the associated Embargo awaiting approval by project admins? - registered_meta dictionary registration supplementary information registration_supplement string registration template ##Links diff --git a/api/search/views.py b/api/search/views.py index 2af2afd79a3..ebf7027ef2f 100644 --- a/api/search/views.py +++ b/api/search/views.py @@ -498,7 +498,6 @@ class SearchRegistrations(BaseSearchView): pending_withdrawal boolean is this registration pending withdrawal? pending_withdrawal_approval boolean is this registration pending approval? pending_embargo_approval boolean is the associated Embargo awaiting approval by project admins? - registered_meta dictionary registration supplementary information registration_supplement string registration template diff --git a/api/sparse/serializers.py b/api/sparse/serializers.py index a085b7344ec..6d08bc97f02 100644 --- a/api/sparse/serializers.py +++ b/api/sparse/serializers.py @@ -167,7 +167,6 @@ def parse_sparse_fields(self): 'pending_registration_approval', 'pending_withdrawal', 'public', - 'registered_meta', 'registration_schema', 'root', 'tags', diff --git a/api_tests/registrations/views/test_registration_list.py b/api_tests/registrations/views/test_registration_list.py index 894c448b5b4..d095ea70b4c 100644 --- a/api_tests/registrations/views/test_registration_list.py +++ b/api_tests/registrations/views/test_registration_list.py @@ -27,7 +27,6 @@ InstitutionFactory, ) from osf_tests.utils import get_default_test_schema -from osf_tests.test_registrations import prereg_registration_responses from rest_framework import exceptions from tests.base import ApiTestCase from website import settings @@ -38,6 +37,77 @@ SCHEMA_VERSION = 2 +prereg_registration_responses = { + 'q12.uploader': [], + 'q7.question': 'data collection procedures', + 'q20': 'transforming and recoding', + 'q21': 'research plan follow up', + 'q22': 'criteria', + 'q23': 'this is how outliers will be handled', + 'q24': 'this is how I will deal with incomplete data.', + 'q25': 'this is my exploratory analysis', + 'q26': [], + 'q27': 'No additional comments', + 'q12.question': 'these are my measured variables', + 'q1': 'This is my title', + 'q3': 'research questions', + 'q5': 'Registration prior to creation of data', + 'q4': 'this is my hypothesis', + 'q6': 'Explanation of existing data', + 'q9': 'this is the rationale for my sample size', + 'q8': 'this is my sample size', + 'q13.question': 'these are my indices', + 'q19.uploader': [], + 'q11.uploader': [ + { + 'file_name': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', + 'file_id': '5d6d22264d476c088fb8e01f', + 'file_urls': { + 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22264d476c088fb8e01f'), + }, + 'file_hashes': { + 'sha256': 'sdf', + }, + }, + { + 'file_name': 'Alphabet.txt', + 'file_id': '5d6d22274d476c088fb8e021', + 'file_urls': { + 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021'), + }, + 'file_hashes': { + 'sha256': 'dsdfds', + }, + } + ], + 'q16.question': 'this is my study design', + 'q15': [ + 'No blinding is involved in this study.', + 'For studies that involve human subjects, they will not know the treatment group to which they have been assigned.', + 'Research personnel who interact directly with the study subjects (either human or non-human subjects) will not be aware of the assigned treatments.' + ], + 'q14': '', + 'q17': 'this is my explanation of randomization', + 'q10': 'this is my stopping rule', + 'q11.question': 'these are my maniuplated variables', + 'q16.uploader': [], + 'q19.question': 'ANOVA', + 'q13.uploader': [], + 'q7.uploader': [ + { + 'file_name': 'Alphabet.txt', + 'file_id': '5d6d22274d476c088fb8e021', + 'file_urls': { + 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021'), + }, + 'file_hashes': { + 'sha256': 'dsdfds', + }, + } + ] +} + + class TestRegistrationList(ApiTestCase): def setUp(self): diff --git a/api_tests/registrations/views/test_view_only_query_parameter.py b/api_tests/registrations/views/test_view_only_query_parameter.py index 2d13ebe26e0..565fdc305c6 100644 --- a/api_tests/registrations/views/test_view_only_query_parameter.py +++ b/api_tests/registrations/views/test_view_only_query_parameter.py @@ -159,23 +159,11 @@ def registration_schema(self): @pytest.fixture() def reg_report(self, registration_schema, admin): - registration = RegistrationFactory(schema=registration_schema, creator=admin, is_public=False) - registration.registered_meta[registration_schema._id] = { - 'q1': { - 'comments': [], - 'extra': [], - 'value': 'This is the answer to a question' - }, - 'q2': { - 'comments': [], - 'extra': [], - 'value': 'Grapes McGee' - } - - } - registration.registration_responses = registration.flatten_registration_metadata() - registration.save() - return registration + return RegistrationFactory( + schema=registration_schema, + creator=admin, + is_public=False + ) @pytest.fixture() def reg_report_anonymous_link(self, reg_report): @@ -189,9 +177,6 @@ def test_author_questions_are_anonymous(self, app, base_url, reg_report, admin, url = f'/{API_BASE}registrations/{reg_report._id}/' res = app.get(url, auth=admin.auth) assert res.status_code == 200 - meta = res.json['data']['attributes']['registered_meta'] - assert 'q1' in meta - assert 'q2' in meta reg_responses = res.json['data']['attributes']['registration_responses'] assert 'q1' in reg_responses @@ -202,9 +187,6 @@ def test_author_questions_are_anonymous(self, app, base_url, reg_report, admin, 'view_only': reg_report_anonymous_link.key }) assert res.status_code == 200 - meta = res.json['data']['attributes']['registered_meta'] - assert 'q1' in meta - assert 'q2' not in meta reg_responses = res.json['data']['attributes']['registration_responses'] assert 'q1' in reg_responses diff --git a/api_tests/registrations/views/test_withdrawn_registrations.py b/api_tests/registrations/views/test_withdrawn_registrations.py index 9cf582e7889..7308f3d4207 100644 --- a/api_tests/registrations/views/test_withdrawn_registrations.py +++ b/api_tests/registrations/views/test_withdrawn_registrations.py @@ -174,7 +174,6 @@ def test_withdrawn_registrations_display_limited_attributes_fields( 'pending_registration_approval': None, 'pending_embargo_approval': None, 'embargo_end_date': None, - 'registered_meta': None, 'current_user_permissions': None, 'registration_supplement': registration.registered_schema.first().name} diff --git a/osf/management/commands/change_node_region.py b/osf/management/commands/change_node_region.py index abce28672bf..5af09e73da9 100644 --- a/osf/management/commands/change_node_region.py +++ b/osf/management/commands/change_node_region.py @@ -41,7 +41,6 @@ def _update_blocks(file_block_map, original_id, cloned_id): def _update_schema_meta(node): logger.info('Updating legacy schema information...') node.registration_responses = node.schema_responses.latest('-created').all_responses - node.registered_meta[node.registration_schema._id] = node.expand_registration_responses() node.save() logger.info('Updated legacy schema information.') diff --git a/osf/management/commands/export_user_account.py b/osf/management/commands/export_user_account.py index deb299c004a..f1f64de49b5 100644 --- a/osf/management/commands/export_user_account.py +++ b/osf/management/commands/export_user_account.py @@ -53,7 +53,6 @@ REGISTRATION_EXPORT_FIELDS = NODE_EXPORT_FIELDS + [ 'registered_data', - 'registered_meta' ] logging.getLogger('urllib3').setLevel(logging.WARNING) diff --git a/osf/management/commands/fix_registration_file_domains.py b/osf/management/commands/fix_registration_file_domains.py index 72529cf3f87..9d8d74817a6 100644 --- a/osf/management/commands/fix_registration_file_domains.py +++ b/osf/management/commands/fix_registration_file_domains.py @@ -48,10 +48,7 @@ def fix_registration_response_file_links(registration): urls['download'] = urls['download'].replace(BAD_DOMAIN, DOMAIN) block.save() - # Re-set registration_responses and registered_meta bssed on the *latest* schema_response, - # which will have inherited these fixes if not already explicitly updated registration.registration_responses = registration.schema_responses.first().all_responses - registration.registered_meta[schema._id] = registration.expand_registration_responses() registration.save() diff --git a/osf/management/commands/migrate_registration_responses.py b/osf/management/commands/migrate_registration_responses.py index 009dee81c4d..74812e3a796 100644 --- a/osf/management/commands/migrate_registration_responses.py +++ b/osf/management/commands/migrate_registration_responses.py @@ -9,7 +9,6 @@ from framework import sentry from osf.exceptions import SchemaBlockConversionError -from osf.utils.registrations import flatten_registration_metadata logger = logging.getLogger(__name__) @@ -20,9 +19,6 @@ def get_nested_responses(registration_or_draft, schema_id): 'registration_metadata', None, ) - if nested_responses is None: - registered_meta = registration_or_draft.registered_meta or {} - nested_responses = registered_meta.get(schema_id, None) return nested_responses # because Registrations and DraftRegistrations are different @@ -82,11 +78,6 @@ def migrate_responses(model, resources, resource_name, dry_run=False, rows='all' errors_to_save = [] for resource in resources: try: - schema = get_registration_schema(resource) - resource.registration_responses = flatten_registration_metadata( - schema, - get_nested_responses(resource, schema._id), - ) resource.registration_responses_migrated = True successes_to_save.append(resource) except SchemaBlockConversionError as e: diff --git a/osf/migrations/0029_remove_abstractnode_registered_meta_and_more.py b/osf/migrations/0029_remove_abstractnode_registered_meta_and_more.py new file mode 100644 index 00000000000..541d21d3907 --- /dev/null +++ b/osf/migrations/0029_remove_abstractnode_registered_meta_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.13 on 2025-04-11 13:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0028_collection_grade_levels_choices_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='abstractnode', + name='registered_meta', + ), + migrations.RemoveField( + model_name='draftregistration', + name='registration_metadata', + ), + ] diff --git a/osf/models/mixins.py b/osf/models/mixins.py index 9027a284f7c..0e614ea1483 100644 --- a/osf/models/mixins.py +++ b/osf/models/mixins.py @@ -44,7 +44,6 @@ ) from osf.utils.permissions import ADMIN, REVIEW_GROUPS, READ, WRITE -from osf.utils.registrations import flatten_registration_metadata, expand_registration_responses from osf.utils.workflows import ( DefaultStates, DefaultTriggers, @@ -2286,37 +2285,6 @@ def file_storage_resource(self): # Where the original files were stored (the node) raise NotImplementedError() - def flatten_registration_metadata(self): - """ - Extracts questions/nested registration_responses - makes use of schema block `registration_response_key` - and block_type to assemble flattened registration_responses. - - For example, if the registration_response_key = "description-methods.planned-sample.question7b", - this will recurse through the registered_meta, looking for each key, starting with "description-methods", - then "planned-sample", and finally "question7b", returning the most deeply nested value corresponding - with the final key to flatten the dictionary. - :self, DraftRegistration or Registration - :returns dictionary, registration_responses, flattened dictionary with registration_response_keys - top-level - """ - schema = self.registration_schema - registered_meta = self.get_registration_metadata(schema) - return flatten_registration_metadata(schema, registered_meta) - - def expand_registration_responses(self): - """ - Expanding `registration_responses` into Draft.registration_metadata or - Registration.registered_meta. registration_responses are more flat; - "registration_response_keys" are top level. Registration_metadata/registered_meta - will have a more deeply nested format. - :returns registration_metadata, dictionary - """ - return expand_registration_responses( - self.registration_schema, - self.registration_responses, - self.file_storage_resource, - ) - class Meta: abstract = True diff --git a/osf/models/node.py b/osf/models/node.py index d06af182e47..115e9e7fb30 100644 --- a/osf/models/node.py +++ b/osf/models/node.py @@ -1439,9 +1439,6 @@ def register_node(self, schema, auth, draft_registration, parent=None, child_ids registered.registered_user = auth.user registered.registered_from = original registered.provider = provider - if not registered.registered_meta: - registered.registered_meta = {} - registered.registered_meta[schema._id] = draft_registration.registration_metadata registered.forked_from = self.forked_from registered.creator = self.creator diff --git a/osf/models/registrations.py b/osf/models/registrations.py index f7b017d9ddf..0db8ff630a0 100644 --- a/osf/models/registrations.py +++ b/osf/models/registrations.py @@ -114,7 +114,6 @@ class Registration(AbstractNode): # TODO: Consider making this a FK, as there can be one per Registration registered_schema = models.ManyToManyField(RegistrationSchema) - registered_meta = DateTimeAwareJSONField(default=dict, blank=True) registered_from = models.ForeignKey('self', related_name='registrations', on_delete=models.SET_NULL, @@ -198,11 +197,6 @@ def registration_schema(self): return self.registered_schema.first() return None - def get_registration_metadata(self, schema): - # Overrides RegistrationResponseMixin - registered_meta = self.registered_meta or {} - return registered_meta.get(schema._id, None) - @property def file_storage_resource(self): # Overrides RegistrationResponseMixin @@ -989,7 +983,6 @@ class DraftRegistration(ObjectIDMixin, RegistrationResponseMixin, DirtyFieldsMix # 'value': # } # } - registration_metadata = DateTimeAwareJSONField(default=dict, blank=True) registration_schema = models.ForeignKey('RegistrationSchema', null=True, on_delete=models.CASCADE) registered_node = models.ForeignKey('Registration', null=True, blank=True, related_name='draft_registration', on_delete=models.CASCADE) @@ -1029,10 +1022,6 @@ def __repr__(self): return ('').format(self=self) - def get_registration_metadata(self, schema): - # Overrides RegistrationResponseMixin - return self.registration_metadata - @property def file_storage_resource(self): # Overrides RegistrationResponseMixin @@ -1282,7 +1271,6 @@ def create_from_node(cls, user, schema, node=None, data=None, provider=None): initiator=user, branched_from=branched_from, registration_schema=schema, - registration_metadata=data or {}, provider=provider, ) draft.save() @@ -1331,14 +1319,6 @@ def copy_contributors_from(self, resource): self.add_permission(contrib.user, permission, save=True) DraftRegistrationContributor.objects.bulk_create(contribs) - def update_metadata(self, metadata): - # Prevent comments on approved drafts - self.registration_metadata.update(metadata) - - # Write to registration_responses also (new workflow) - registration_responses = self.flatten_registration_metadata() - self.registration_responses.update(registration_responses) - def update_registration_responses(self, registration_responses): """ New workflow - update_registration_responses. This should have been @@ -1347,8 +1327,6 @@ def update_registration_responses(self, registration_responses): """ registration_responses = self.unescape_registration_file_names(registration_responses) self.registration_responses.update(registration_responses) - registration_metadata = self.expand_registration_responses() - self.registration_metadata = registration_metadata return def unescape_registration_file_names(self, registration_responses): diff --git a/osf/utils/registrations.py b/osf/utils/registrations.py index 3e8adfa1069..71e247acf38 100644 --- a/osf/utils/registrations.py +++ b/osf/utils/registrations.py @@ -1,49 +1,9 @@ -import copy import re from urllib.parse import urljoin -from osf.exceptions import SchemaBlockConversionError from website import settings - -def strip_registered_meta_comments(messy_dict_or_list, in_place=False): - """Removes Prereg Challenge comments from a given `registered_meta` dict. - - Nothing publicly exposed needs these comments: - ``` - { - "registered_meta": { - "q20": { - "comments": [ ... ], <~~~ THIS - "value": "foo", - "extra": [] - }, - } - } - ``` - - If `in_place` is truthy, modifies `messy_dict_or_list` and returns it. - Else, returns a deep copy without modifying the given `messy_dict_or_list` - """ - obj = messy_dict_or_list if in_place else copy.deepcopy(messy_dict_or_list) - - if isinstance(obj, list): - for nested_obj in obj: - strip_registered_meta_comments(nested_obj, in_place=True) - elif isinstance(obj, dict): - comments = obj.get('comments', None) - - # some schemas have a question named "comments" -- those will have a dict value - if isinstance(comments, list): - del obj['comments'] - - # dig into the deeply nested structure - for nested_obj in obj.values(): - strip_registered_meta_comments(nested_obj, in_place=True) - return obj - - """ Old workflow uses DraftRegistration.registration_metadata and Registration.registered_meta. New workflow uses DraftRegistration.registration_responses and Registration.registration_responses. @@ -62,296 +22,3 @@ def strip_registered_meta_comments(messy_dict_or_list, in_place=False): # use absolute URLs in new 'flattened' format FILE_HTML_URL_TEMPLATE = urljoin(settings.DOMAIN, '/project/{node_id}/files/osfstorage/{file_id}') FILE_DOWNLOAD_URL_TEMPLATE = urljoin(settings.DOMAIN, '/download/{file_id}') - -def flatten_registration_metadata(schema, registered_meta): - """ - Extracts questions/nested registration_responses - makes use of schema block `registration_response_key` - and block_type to assemble flattened registration_responses. - - For example, if the registration_response_key = "description-methods.planned-sample.question7b", - this will recurse through the registered_meta, looking for each key, starting with "description-methods", - then "planned-sample", and finally "question7b", returning the most deeply nested value corresponding - with the final key to flatten the dictionary. - :schema, RegistrationSchema instance - :registered_meta, dict containing the legacy "nested" registered_meta/registration_metadata - :returns dictionary, registration_responses, flattened dictionary with registration_response_keys - top-level - """ - registration_responses = {} - registration_response_keys = schema.schema_blocks.filter( - registration_response_key__isnull=False - ).values( - 'registration_response_key', - 'block_type' - ) - - for registration_response_key_dict in registration_response_keys: - key = registration_response_key_dict['registration_response_key'] - registration_responses[key] = get_nested_answer( - registered_meta, - registration_response_key_dict['block_type'], - key.split('.') - ) - return registration_responses - -# For flatten_registration_metadata -def build_file_ref(file): - """ - Extracts name, file_id, and sha256 from the nested "extras" dictionary. - Pulling name from selectedFileName and the file_id from the viewUrl. - - Some weird data here...such as {u'selectedFileName': u'No file selected'} - Raises SchemaBlockConversionError if it's too weird to handle. - - :returns dictionary formatted as below - { - file_id: , - file_name: , - file_hashes: { - sha256: , - }, - file_urls: { - html: , - download: , - }, - } - """ - file_data = file.get('data') - - # on a Registration, viewUrl is the only place the file/node ids are accurate. - # the others refer to the original file on the node, not the file that was archived on the reg - view_url = file.get('viewUrl') - file_id = None - node_id = None - if view_url: - url_match = FILE_VIEW_URL_REGEX.search(view_url) - if not url_match: - raise SchemaBlockConversionError(f'Unexpected file viewUrl: {view_url}') - groupdict = url_match.groupdict() - file_id = groupdict['file_id'] - node_id = groupdict['node_id'] - elif file_data: - # this data is bad and it should feel bad - id_from_path = file_data.get('path', '').lstrip('/') - file_id = id_from_path or file.get('fileId') - node_id = file.get('nodeId') - - if not (file_id and node_id): - raise SchemaBlockConversionError(f'Could not find file and node ids in file info: {file}') - - file_name = file.get('selectedFileName') - if file_data and not file_name: - file_name = file_data.get('name') - - sha256 = file.get('sha256') - if file_data and not sha256: - sha256 = file_data.get('extra', {}).get('hashes', {}).get('sha256') - - return { - 'file_id': file_id, - 'file_name': file_name, - 'file_hashes': {'sha256': sha256} if sha256 else {}, - 'file_urls': { - 'html': FILE_HTML_URL_TEMPLATE.format(file_id=file_id, node_id=node_id), - 'download': FILE_DOWNLOAD_URL_TEMPLATE.format(file_id=file_id), - }, - } - -# For flatten_registration_metadata -def build_file_refs(messy_file_infos): - for file_info in messy_file_infos: - if not file_info: - continue - if len(file_info) == 1 and file_info.get('selectedFileName') == 'No file selected': - continue - yield build_file_ref(file_info) - -# For flatten_registration_metadata -def get_value_or_extra(nested_response, block_type, key, keys): - """ - Sometimes the relevant information is stored under "extra" for files, - otherwise, "value". - - :params, nested dictionary - :block_type, string, current block type - :key, particular key in question - :keys, array of keys remaining to recurse through to find the user's answer - :returns array (files or multi-response answers) or a string IF deepest level of nesting, - otherwise, returns a dictionary to get the next level of nesting. - """ - keyed_value = nested_response.get(key, '') - # No guarantee that the key exists in the dictionary - if isinstance(keyed_value, str): - return keyed_value - - # If we are on the most deeply nested key (no more keys left in array), - # and the block type is "file-input", the information we want is - # stored under extra - if block_type == 'file-input' and not keys: - extra = keyed_value.get('extra', []) - extra_list = extra if isinstance(extra, list) else [extra] - return list(build_file_refs(extra_list)) - - value = keyed_value.get('value') - if value is None: - return '' - if isinstance(value, bool): - return str(value) - return value - -# For flatten_registration_metadata -def get_nested_answer(nested_response, block_type, keys): - """ - Recursively fetches the nested response in registered_meta. - - :params nested_response dictionary - :params keys array, of nested question_ids: ["recommended-analysis", "specify", "question11c"] - :returns array (files or multi-response answers) or a string - """ - if isinstance(nested_response, dict): - if not keys: - raise SchemaBlockConversionError('Unexpected nested object (expected list or string)', nested_response) - key = keys.pop(0) - # Returns the value associated with the given key - value = get_value_or_extra(nested_response, block_type, key, keys) - return get_nested_answer(value, block_type, keys) - else: - # Once we've drilled down through the entire dictionary, our nested_response - # should be an array or a string - if not isinstance(nested_response, (list, str)): - raise SchemaBlockConversionError('Unexpected value type (expected list or string)', nested_response) - return nested_response - -def expand_registration_responses(schema, registration_responses, file_storage_resource): - """ - Expanding `registration_responses` into Draft.registration_metadata or - Registration.registered_meta. registration_responses are more flat; - "registration_response_keys" are top level. Registration_metadata/registered_meta - will have a more deeply nested format. - :returns registration_metadata, dictionary - """ - registration_responses = copy.deepcopy(registration_responses) - - # Pull out all registration_response_keys and their block types - registration_response_keys = schema.schema_blocks.filter( - registration_response_key__isnull=False - ).values( - 'registration_response_key', - 'block_type' - ) - - metadata = {} - - for registration_response_key_dict in registration_response_keys: - response_key = str(registration_response_key_dict['registration_response_key']) - # Turns "confirmatory-analyses-further.further.question2c" into - # ['confirmatory-analyses-further', 'value', 'further', 'value', 'question2c'] - nested_keys = response_key.replace('.', '.value.').split('.') - block_type = registration_response_key_dict['block_type'] - - # Continues to add to metadata with every registration_response_key - metadata = build_registration_metadata_dict( - nested_keys, - metadata=metadata, - value=build_answer_block( - block_type, - registration_responses.get(response_key, ''), - file_storage_resource=file_storage_resource - ) - ) - return metadata - -# For expanding registration_responses -def set_nested_values(nested_dictionary, keys, value): - """ - Drills down through the nested dictionary, accessing each key in the array, - and sets the last key equal to the passed in value, if this key doesn't already exist. - - Assumes all keys are present, except for potentially the final key. - - :param nested_dictionary, dictionary - :param keys, array - :param value, object, array, or string - """ - for key in keys[:-1]: - nested_dictionary = nested_dictionary.get(key, None) - - final_key = keys[-1] - if not nested_dictionary.get(final_key): - nested_dictionary[final_key] = value - -# For expanding registration_responses -def build_extra_file_dict(file_ref): - url_match = FILE_VIEW_URL_REGEX.search(file_ref['file_urls']['html']) - if not url_match: - raise SchemaBlockConversionError('Expected `file_urls.html` in format `/project//files/osfstorage/`') - - groups = url_match.groupdict() - node_id = groups['node_id'] - file_id = groups['file_id'] - - file_name = file_ref['file_name'] - sha256 = file_ref['file_hashes']['sha256'] - - # viewUrl, selectedFileName, and sha256 are everything needed for the return trip - # (see `osf.utils.build_file_ref`) - return { - 'viewUrl': FILE_VIEW_URL_TEMPLATE.format(node_id=node_id, file_id=file_id), - 'selectedFileName': file_name, - 'sha256': sha256, - 'nodeId': node_id, # Used in _find_orphan_files - 'data': { - 'name': file_name, # What legacy FE needs for rendering file on the draft - } - } - -# For expanding registration_responses -def build_answer_block(block_type, value, file_storage_resource=None): - extra = [] - if block_type == 'file-input': - extra = list(map(build_extra_file_dict, value)) - value = '' - return { - 'comments': [], - 'value': value, - 'extra': extra - } - -# For expanding registration_responses -def build_registration_metadata_dict(keys, current_index=0, metadata={}, value={}): - """ - Function will recursively loop through each key in the list, checking if it exists in metadata, if not, - adding another nested level in the dictionary. - - For example, calling build_registration_metadata_dict( - ["recommended-analysis", "value", "specify", "value", "question11c"], value='hello'), yields, - - { - 'recommended-analysis': { - 'value': { - 'specify': { - 'value': { - 'question11c': 'hello' - } - } - } - } - } - :param keys array, of nested question_ids: ["recommended-analysis", "value", "specify", "value", "question11c"] - :param current_index, call initially with 0 - :param metadata - registration_metadata - :param value, provide most deeply nested value. - :returns partial registration_metadata - """ - if current_index == len(keys): - # We've iterated through all the keys, so we exit. - return metadata - else: - # All keys from left to right including the current key. - current_chain = keys[0:current_index + 1] - # If we're on the final key, use the passed in value - val = value if current_index == len(keys) - 1 else {} - # set_nested_values incrementally adds another layer of nesting to the dictionary, - # until we get to the deepest level where we can set the value equal to the user's response - set_nested_values(metadata, current_chain, val) - return build_registration_metadata_dict(keys, current_index + 1, metadata, value) diff --git a/osf_tests/factories.py b/osf_tests/factories.py index 7ad8885e1ad..37ca8096f02 100644 --- a/osf_tests/factories.py +++ b/osf_tests/factories.py @@ -587,7 +587,6 @@ def _create(cls, *args, **kwargs): draft.title = title if description: draft.description = description - draft.registration_responses = draft.flatten_registration_metadata() draft.save() return draft diff --git a/osf_tests/management_commands/test_migration_registration_responses.py b/osf_tests/management_commands/test_migration_registration_responses.py deleted file mode 100644 index 880590717d1..00000000000 --- a/osf_tests/management_commands/test_migration_registration_responses.py +++ /dev/null @@ -1,2084 +0,0 @@ -import pytest -from urllib.parse import urljoin - -from osf.models import RegistrationSchema -from osf_tests.factories import DraftRegistrationFactory, RegistrationFactory - -from osf.management.commands.migrate_registration_responses import ( - migrate_draft_registrations, - migrate_registrations -) -from website import settings - -""" -Regression test to prevent the migration of 'registration_metadata' behavior -from breaking -""" - -prereg_registration_responses = { - 'q12.uploader': [], - 'q7.question': 'data collection procedures', - 'q20': 'transforming and recoding', - 'q21': 'research plan follow up', - 'q22': 'criteria', - 'q23': 'this is how outliers will be handled', - 'q24': 'this is how I will deal with incomplete data.', - 'q25': 'this is my exploratory analysis', - 'q26': [], - 'q27': 'No additional comments', - 'q12.question': 'these are my measured variables', - 'q1': 'This is my title', - 'q3': 'research questions', - 'q5': 'Registration prior to creation of data', - 'q4': 'this is my hypothesis', - 'q6': 'Explanation of existing data', - 'q9': 'this is the rationale for my sample size', - 'q8': 'this is my sample size', - 'q13.question': 'these are my indices', - 'q19.uploader': [], - 'q11.uploader': [ - { - 'file_name': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'file_id': '5d6d22264d476c088fb8e01f', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22264d476c088fb8e01f'), - }, - 'file_hashes': { - 'sha256': 'sdf', - }, - }, - { - 'file_name': 'Alphabet.txt', - 'file_id': '5d6d22274d476c088fb8e021', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021'), - }, - 'file_hashes': { - 'sha256': 'dsdfds', - }, - } - ], - 'q16.question': 'this is my study design', - 'q15': [ - 'No blinding is involved in this study.', - 'For studies that involve human subjects, they will not know the treatment group to which they have been assigned.', - 'Research personnel who interact directly with the study subjects (either human or non-human subjects) will not be aware of the assigned treatments.' - ], - 'q14': '', - 'q17': 'this is my explanation of randomization', - 'q10': 'this is my stopping rule', - 'q11.question': 'these are my maniuplated variables', - 'q16.uploader': [], - 'q19.question': 'ANOVA', - 'q13.uploader': [], - 'q7.uploader': [ - { - 'file_name': 'Alphabet.txt', - 'file_id': '5d6d22274d476c088fb8e021', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021'), - }, - 'file_hashes': { - 'sha256': 'dsdfds', - }, - } - ] -} - -prereg_registration_metadata = { - 'q20': { - 'comments': [], - 'value': 'transforming and recoding', - 'extra': [] - }, - 'q21': { - 'comments': [], - 'value': 'research plan follow up', - 'extra': [] - }, - 'q22': { - 'comments': [], - 'value': 'criteria', - 'extra': [] - }, - 'q23': { - 'comments': [], - 'value': 'this is how outliers will be handled', - 'extra': [] - }, - 'q24': { - 'comments': [], - 'value': 'this is how I will deal with incomplete data.', - 'extra': [] - }, - 'q25': { - 'comments': [], - 'value': 'this is my exploratory analysis', - 'extra': [] - }, - 'q26': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'q27': { - 'comments': [], - 'value': 'No additional comments', - 'extra': [] - }, - 'q1': { - 'comments': [], - 'value': 'This is my title', - 'extra': [] - }, - 'q3': { - 'comments': [], - 'value': 'research questions', - 'extra': [] - }, - 'q5': { - 'comments': [], - 'value': 'Registration prior to creation of data', - 'extra': [] - }, - 'q4': { - 'comments': [], - 'value': 'this is my hypothesis', - 'extra': [] - }, - 'q7': { - 'comments': [], - 'value': { - 'question': { - 'comments': [], - 'value': 'data collection procedures', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': 'Alphabet.txt', - 'extra': [ - { - 'descriptionValue': '', - 'nodeId': '9bknu', - 'viewUrl': '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021/', - 'selectedFileName': 'Alphabet.txt', - 'sha256': 'dsdfds', - 'data': { - 'contentType': '', - 'sizeInt': 3169, - 'kind': 'file', - 'resource': '9bknu', - 'name': 'Alphabet.txt', - 'extra': { - 'downloads': 0, - 'latestVersionSeen': '', - 'version': 1, - 'hashes': { - 'sha256': 'dsdfds', - 'md5': 'sdfsef' - }, - 'guid': '', - 'checkout': '' - }, - 'materialized': '/Alphabet.txt', - 'created_utc': '', - 'nodeId': '9bknu', - 'modified': '2019-09-02T14:04:14.776301+00:00', - 'etag': 'b529584199f707abda42a42ec2f3c5a052d280e56fb3382f07efc77933053159', - 'provider': 'osfstorage', - 'path': '/5d6d215b4d476c088fb8e000', - 'modified_utc': '2019-09-02T14:04:14+00:00', - 'size': 3169 - }, - 'fileId': '7ry42' - } - ] - } - }, - 'extra': [] - }, - 'q6': { - 'comments': [], - 'value': 'Explanation of existing data', - 'extra': [] - }, - 'q9': { - 'comments': [], - 'value': 'this is the rationale for my sample size', - 'extra': [] - }, - 'q8': { - 'comments': [], - 'value': 'this is my sample size', - 'extra': [] - }, - 'q15': { - 'comments': [], - 'value': [ - 'No blinding is involved in this study.', - 'For studies that involve human subjects, they will not know the treatment group to which they have been assigned.', - 'Research personnel who interact directly with the study subjects (either human or non-human subjects) will not be aware of the assigned treatments.' - ], - 'extra': [] - }, - 'q14': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'q17': { - 'comments': [], - 'value': 'this is my explanation of randomization', - 'extra': [] - }, - 'q16': { - 'comments': [], - 'value': { - 'question': { - 'comments': [], - 'value': 'this is my study design', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': '', - 'extra': [] - } - }, - 'extra': [] - }, - 'q11': { - 'comments': [], - 'value': { - 'question': { - 'comments': [], - 'value': 'these are my maniuplated variables', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'extra': [ - { - 'descriptionValue': '', - 'nodeId': '9bknu', - 'viewUrl': '/project/57zbh/files/osfstorage/5d6d22264d476c088fb8e01f/', - 'selectedFileName': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'sha256': 'sdf', - 'data': { - 'contentType': '', - 'sizeInt': 78257, - 'kind': 'file', - 'resource': '9bknu', - 'name': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'extra': { - 'downloads': 0, - 'latestVersionSeen': '', - 'version': 1, - 'hashes': { - 'sha256': 'sdf', - 'md5': 'asdf' - }, - 'guid': '', - 'checkout': '' - }, - 'materialized': '/Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'created_utc': '', - 'nodeId': '9bknu', - 'modified': '2019-09-02T14:04:47.793337+00:00', - 'etag': 'd114466ad8f1e04c03e678fdaf642988c1accd1526bb6e81b34bf35612a77cbb', - 'provider': 'osfstorage', - 'path': '/5d6d217f4d476c088fb8e00d', - 'modified_utc': '2019-09-02T14:04:47+00:00', - 'size': 78257 - }, - 'fileId': 'nc8qy' - }, - { - 'descriptionValue': '', - 'nodeId': '9bknu', - 'viewUrl': '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021/', - 'selectedFileName': 'Alphabet.txt', - 'sha256': 'asdf', - 'data': { - 'links': { - 'download': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d215b4d476c088fb8e000', - 'move': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d215b4d476c088fb8e000', - 'upload': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d215b4d476c088fb8e000?kind=file', - 'delete': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d215b4d476c088fb8e000' - }, - 'extra': { - 'downloads': 0, - 'latestVersionSeen': '', - 'version': 1, - 'hashes': { - 'sha256': 'asdf', - 'md5': 'sdfs' - }, - 'guid': '', - 'checkout': '' - }, - 'accept': { - 'acceptedFiles': 'True', - 'maxSize': 5120 - }, - 'id': 'osfstorage/5d6d215b4d476c088fb8e000', - 'size': 3169, - 'nodeApiUrl': '/api/v1/project/9bknu/', - 'nodeId': '9bknu', - 'etag': 'b529584199f707abda42a42ec2f3c5a052d280e56fb3382f07efc77933053159', - 'provider': 'osfstorage', - 'type': 'files', - 'nodeUrl': '/9bknu/', - 'sizeInt': 3169, - 'contentType': '', - 'path': '/5d6d215b4d476c088fb8e000', - 'permissions': { - 'edit': 'True', - 'view': 'True' - }, - 'waterbutlerURL': 'http://localhost:7777', - 'kind': 'file', - 'resource': '9bknu', - 'name': 'Alphabet.txt', - 'materialized': '/Alphabet.txt', - 'created_utc': '2019-09-02T14:04:14.776301+00:00', - 'modified': '2019-09-02T14:04:14.776301+00:00', - 'modified_utc': '2019-09-02T14:04:14.776301+00:00' - }, - 'fileId': '7ry42' - } - ] - } - }, - 'extra': [] - }, - 'q10': { - 'comments': [], - 'value': 'this is my stopping rule', - 'extra': [] - }, - 'q13': { - 'comments': [], - 'value': { - 'question': { - 'comments': [], - 'value': 'these are my indices', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': '', - 'extra': [] - } - }, - 'extra': [] - }, - 'q12': { - 'comments': [], - 'value': { - 'question': { - 'comments': [], - 'value': 'these are my measured variables', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': '', - 'extra': [] - } - }, - 'extra': [] - }, - 'q19': { - 'comments': [], - 'value': { - 'question': { - 'comments': [], - 'value': 'ANOVA', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': '', - 'extra': [] - } - }, - 'extra': [] - } -} - -prereg_registration_metadata_built = { - 'q20': { - 'comments': [], - 'value': 'transforming and recoding', - 'extra': [] - }, - 'q21': { - 'comments': [], - 'value': 'research plan follow up', - 'extra': [] - }, - 'q22': { - 'comments': [], - 'value': 'criteria', - 'extra': [] - }, - 'q23': { - 'comments': [], - 'value': 'this is how outliers will be handled', - 'extra': [] - }, - 'q24': { - 'comments': [], - 'value': 'this is how I will deal with incomplete data.', - 'extra': [] - }, - 'q25': { - 'comments': [], - 'value': 'this is my exploratory analysis', - 'extra': [] - }, - 'q26': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'q27': { - 'comments': [], - 'value': 'No additional comments', - 'extra': [] - }, - 'q1': { - 'comments': [], - 'value': 'This is my title', - 'extra': [] - }, - 'q3': { - 'comments': [], - 'value': 'research questions', - 'extra': [] - }, - 'q5': { - 'comments': [], - 'value': 'Registration prior to creation of data', - 'extra': [] - }, - 'q4': { - 'comments': [], - 'value': 'this is my hypothesis', - 'extra': [] - }, - 'q7': { - 'value': { - 'question': { - 'comments': [], - 'value': 'data collection procedures', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': '', - 'extra': [ - { - 'viewUrl': '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021', - 'selectedFileName': 'Alphabet.txt', - 'nodeId': '57zbh', - 'sha256': 'dsdfds', - 'data': { - 'name': 'Alphabet.txt' - } - } - ] - } - }, - }, - 'q6': { - 'comments': [], - 'value': 'Explanation of existing data', - 'extra': [] - }, - 'q9': { - 'comments': [], - 'value': 'this is the rationale for my sample size', - 'extra': [] - }, - 'q8': { - 'comments': [], - 'value': 'this is my sample size', - 'extra': [] - }, - 'q15': { - 'comments': [], - 'value': [ - 'No blinding is involved in this study.', - 'For studies that involve human subjects, they will not know the treatment group to which they have been assigned.', - 'Research personnel who interact directly with the study subjects (either human or non-human subjects) will not be aware of the assigned treatments.' - ], - 'extra': [] - }, - 'q14': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'q17': { - 'comments': [], - 'value': 'this is my explanation of randomization', - 'extra': [] - }, - 'q16': { - 'value': { - 'question': { - 'comments': [], - 'value': 'this is my study design', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': '', - 'extra': [] - } - }, - }, - 'q11': { - 'value': { - 'question': { - 'comments': [], - 'value': 'these are my maniuplated variables', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': '', - 'extra': [ - { - 'selectedFileName': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'viewUrl': '/project/57zbh/files/osfstorage/5d6d22264d476c088fb8e01f', - 'nodeId': '57zbh', - 'sha256': 'sdf', - 'data': { - 'name': 'Screen Shot 2019-08-30 at 9.04.01 AM.png' - } - }, - { - 'selectedFileName': 'Alphabet.txt', - 'viewUrl': '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021', - 'nodeId': '57zbh', - 'sha256': 'dsdfds', - 'data': { - 'name': 'Alphabet.txt' - } - } - ] - } - }, - }, - 'q10': { - 'comments': [], - 'value': 'this is my stopping rule', - 'extra': [] - }, - 'q13': { - 'value': { - 'question': { - 'comments': [], - 'value': 'these are my indices', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': '', - 'extra': [] - } - }, - }, - 'q12': { - 'value': { - 'question': { - 'comments': [], - 'value': 'these are my measured variables', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': '', - 'extra': [] - } - }, - }, - 'q19': { - 'value': { - 'question': { - 'comments': [], - 'value': 'ANOVA', - 'extra': [] - }, - 'uploader': { - 'comments': [], - 'value': '', - 'extra': [] - } - }, - } -} - -veer_registration_metadata = { - 'dataCollectionDates': { - 'comments': [], - 'value': '2020 - 2030', - 'extra': [] - }, - 'description-methods': { - 'comments': [], - 'value': { - 'exclusion-criteria': { - 'comments': [], - 'value': { - 'question8b': { - 'comments': [], - 'value': 'these are failing check-tests', - 'extra': [] - } - }, - 'extra': [] - }, - 'design': { - 'comments': [], - 'value': { - 'question2a': { - 'comments': [], - 'value': 'a. whether they are between participants', - 'extra': [] - }, - 'question2b': { - 'comments': [], - 'value': 'these are my dependent variables', - 'extra': [] - }, - 'question3b': { - 'comments': [], - 'value': 'These variables are acting as covariates.', - 'extra': [] - } - }, - 'extra': [] - }, - 'procedure': { - 'comments': [], - 'value': { - 'question10b': { - 'comments': [], - 'value': 'describe all manipulations', - 'extra': [] - } - }, - 'extra': [] - }, - 'planned-sample': { - 'comments': [], - 'value': { - 'question4b': { - 'comments': [], - 'value': 'these are the preselection rults', - 'extra': [] - }, - 'question7b': { - 'comments': [], - 'value': 'here is my data collection termination rule', - 'extra': [] - }, - 'question5b': { - 'comments': [], - 'value': 'here is how the data will be collected', - 'extra': [] - }, - 'question6b-upload': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question6b': { - 'comments': [], - 'value': 'this is my planned sample size', - 'extra': [] - } - }, - 'extra': [] - } - }, - 'extra': [] - }, - 'recommended-analysis': { - 'comments': [], - 'value': { - 'specify': { - 'comments': [], - 'value': { - 'question6c': { - 'comments': [], - 'value': 'I used a method of correction for multiple tests', - 'extra': [] - }, - 'question8c': { - 'comments': [], - 'value': 'reliability criteria', - 'extra': [] - }, - 'question9c': { - 'comments': [], - 'value': 'these are the anticipated data transformations', - 'extra': [] - }, - 'question7c': { - 'comments': [], - 'value': 'method of missing data handling', - 'extra': [] - }, - 'question11c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question10c': { - 'comments': [], - 'value': 'assumptions of analysses', - 'extra': [] - } - }, - 'extra': [] - } - }, - 'extra': [] - }, - 'description-hypothesis': { - 'comments': [], - 'value': { - 'question2a': { - 'comments': [], - 'value': 'expected interaction shape', - 'extra': [] - }, - 'question1a': { - 'comments': [], - 'value': 'These are the essential elements', - 'extra': [] - }, - 'question3a': { - 'comments': [], - 'value': 'predictions for successful checks', - 'extra': [] - } - }, - 'extra': [] - }, - 'confirmatory-analyses-first': { - 'comments': [], - 'value': { - 'first': { - 'comments': [], - 'value': { - 'question4c': { - 'comments': [], - 'value': 'this the covariate rationale', - 'extra': [] - }, - 'question5c': { - 'comments': [], - 'value': 'these are techniques for null hypo testing', - 'extra': [] - }, - 'question2c': { - 'comments': [], - 'value': 'this is the statistical technicque', - 'extra': [] - }, - 'question3c': { - 'comments': [], - 'value': 'this is each variable role', - 'extra': [] - }, - 'question1c': { - 'comments': [], - 'value': 'these are the relevant variables', - 'extra': [] - } - }, - 'extra': [] - } - }, - 'extra': [] - }, - 'confirmatory-analyses-third': { - 'comments': [], - 'value': { - 'third': { - 'comments': [], - 'value': { - 'question4c': { - 'comments': [], - 'value': 'here was the rationale', - 'extra': [] - }, - 'question5c': { - 'comments': [], - 'value': 'I used BAYESIAN STATISTICS', - 'extra': [] - }, - 'question2c': { - 'comments': [], - 'value': 't-test', - 'extra': [] - }, - 'question3c': { - 'comments': [], - 'value': 't-test informed covariate', - 'extra': [] - }, - 'question1c': { - 'comments': [], - 'value': '3rd prediction', - 'extra': [] - } - }, - 'extra': [] - } - }, - 'extra': [] - }, - 'datacompletion': { - 'comments': [], - 'value': 'No, data collection has not begun', - 'extra': [] - }, - 'looked': { - 'comments': [], - 'value': 'Yes', - 'extra': [] - }, - 'confirmatory-analyses-fourth': { - 'comments': [], - 'value': { - 'fourth': { - 'comments': [], - 'value': { - 'question4c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question5c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question2c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question3c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question1c': { - 'comments': [], - 'value': '', - 'extra': [] - } - }, - 'extra': [] - } - }, - 'extra': [] - }, - 'confirmatory-analyses-further': { - 'comments': [], - 'value': { - 'further': { - 'comments': [], - 'value': { - 'question4c': { - 'comments': [], - 'value': 'this was the rationale', - 'extra': [] - }, - 'question5c': { - 'comments': [], - 'value': 'also Bayesian', - 'extra': [] - }, - 'question2c': { - 'comments': [], - 'value': 'i used a common statistical technique', - 'extra': [] - }, - 'question3c': { - 'comments': [], - 'value': 'this was the independent variable', - 'extra': [] - }, - 'question1c': { - 'comments': [], - 'value': 'FURTHER PREdictions:', - 'extra': [] - } - }, - 'extra': [] - } - }, - 'extra': [] - }, - 'recommended-hypothesis': { - 'comments': [], - 'value': { - 'question5a': { - 'comments': [], - 'value': 'This is the hypotheses that was tested.', - 'extra': [] - }, - 'question4a': { - 'comments': [], - 'value': 'Alphabet.txt', - 'extra': [ - { - 'descriptionValue': '', - 'nodeId': '9bknu', - 'viewUrl': '/project/85qku/files/osfstorage/5d6d25024d476c088fb8e03b/', - 'selectedFileName': 'Alphabet.txt', - 'sha256': 'asdf', - 'data': { - 'links': { - 'download': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d215b4d476c088fb8e000', - 'move': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d215b4d476c088fb8e000', - 'upload': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d215b4d476c088fb8e000?kind=file', - 'delete': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d215b4d476c088fb8e000' - }, - 'extra': { - 'downloads': 0, - 'latestVersionSeen': '', - 'version': 1, - 'hashes': { - 'sha256': 'asdf', - 'md5': 'asd' - }, - 'guid': '', - 'checkout': '' - }, - 'accept': { - 'acceptedFiles': 'True', - 'maxSize': 5120 - }, - 'id': 'osfstorage/5d6d215b4d476c088fb8e000', - 'size': 3169, - 'nodeApiUrl': '/api/v1/project/9bknu/', - 'nodeId': '9bknu', - 'etag': 'b529584199f707abda42a42ec2f3c5a052d280e56fb3382f07efc77933053159', - 'provider': 'osfstorage', - 'type': 'files', - 'nodeUrl': '/9bknu/', - 'sizeInt': 3169, - 'contentType': '', - 'path': '/5d6d215b4d476c088fb8e000', - 'permissions': { - 'edit': 'True', - 'view': 'True' - }, - 'waterbutlerURL': 'http://localhost:7777', - 'kind': 'file', - 'resource': '9bknu', - 'name': 'Alphabet.txt', - 'materialized': '/Alphabet.txt', - 'created_utc': '2019-09-02T14:04:14.776301+00:00', - 'modified': '2019-09-02T14:04:14.776301+00:00', - 'modified_utc': '2019-09-02T14:04:14.776301+00:00' - }, - 'fileId': '7ry42' - }, - { - 'descriptionValue': '', - 'nodeId': '9bknu', - 'viewUrl': '/project/85qku/files/osfstorage/5d6d25014d476c088fb8e038/', - 'selectedFileName': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'sha256': 'asdf', - 'data': { - 'links': { - 'download': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d217f4d476c088fb8e00d', - 'move': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d217f4d476c088fb8e00d', - 'upload': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d217f4d476c088fb8e00d?kind=file', - 'delete': 'http://localhost:7777/v1/resources/9bknu/providers/osfstorage/5d6d217f4d476c088fb8e00d' - }, - 'extra': { - 'downloads': 0, - 'latestVersionSeen': '', - 'version': 1, - 'hashes': { - 'sha256': 'asdf', - 'md5': 'asd' - }, - 'guid': '', - 'checkout': '' - }, - 'accept': { - 'acceptedFiles': 'True', - 'maxSize': 5120 - }, - 'id': 'osfstorage/5d6d217f4d476c088fb8e00d', - 'size': 78257, - 'nodeApiUrl': '/api/v1/project/9bknu/', - 'nodeId': '9bknu', - 'etag': 'd114466ad8f1e04c03e678fdaf642988c1accd1526bb6e81b34bf35612a77cbb', - 'provider': 'osfstorage', - 'type': 'files', - 'nodeUrl': '/9bknu/', - 'sizeInt': 78257, - 'contentType': '', - 'path': '/5d6d217f4d476c088fb8e00d', - 'permissions': { - 'edit': 'True', - 'view': 'True' - }, - 'waterbutlerURL': 'http://localhost:7777', - 'kind': 'file', - 'resource': '9bknu', - 'name': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'materialized': '/Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'created_utc': '2019-09-02T14:04:47.793337+00:00', - 'modified': '2019-09-02T14:04:47.793337+00:00', - 'modified_utc': '2019-09-02T14:04:47.793337+00:00' - }, - 'fileId': 'nc8qy' - } - ] - }, - 'question6a': { - 'comments': [], - 'value': 'this is the outcome that would be predicted by each theory', - 'extra': [] - } - }, - 'extra': [] - }, - 'additionalComments': { - 'comments': [], - 'value': 'no additional comments', - 'extra': [] - }, - 'confirmatory-analyses-second': { - 'comments': [], - 'value': { - 'second': { - 'comments': [], - 'value': { - 'question4c': { - 'comments': [], - 'value': 'here is the rationale', - 'extra': [] - }, - 'question5c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question2c': { - 'comments': [], - 'value': 'ANOVA test', - 'extra': [] - }, - 'question3c': { - 'comments': [], - 'value': 'it was the covariate', - 'extra': [] - }, - 'question1c': { - 'comments': [], - 'value': 'how 2nd prediction calculated', - 'extra': [] - } - }, - 'extra': [] - } - }, - 'extra': [] - }, - 'recommended-methods': { - 'comments': [], - 'value': { - 'procedure': { - 'comments': [], - 'value': { - 'question9b-file': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question9b': { - 'comments': [], - 'value': 'set fail-safe levels of exclusions', - 'extra': [] - } - }, - 'extra': [] - } - }, - 'extra': [] - } -} - -veer_condensed = { - 'dataCollectionDates': { - 'comments': [], - 'value': '2020 - 2030', - 'extra': [] - }, - 'description-methods': { - 'value': { - 'exclusion-criteria': { - 'value': { - 'question8b': { - 'comments': [], - 'value': 'these are failing check-tests', - 'extra': [] - } - }, - }, - 'design': { - 'value': { - 'question2a': { - 'comments': [], - 'value': 'a. whether they are between participants', - 'extra': [] - }, - 'question2b': { - 'comments': [], - 'value': 'these are my dependent variables', - 'extra': [] - }, - 'question3b': { - 'comments': [], - 'value': 'These variables are acting as covariates.', - 'extra': [] - } - }, - }, - 'procedure': { - 'value': { - 'question10b': { - 'comments': [], - 'value': 'describe all manipulations', - 'extra': [] - } - }, - }, - 'planned-sample': { - 'value': { - 'question4b': { - 'comments': [], - 'value': 'these are the preselection rults', - 'extra': [] - }, - 'question7b': { - 'comments': [], - 'value': 'here is my data collection termination rule', - 'extra': [] - }, - 'question5b': { - 'comments': [], - 'value': 'here is how the data will be collected', - 'extra': [] - }, - 'question6b-upload': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question6b': { - 'comments': [], - 'value': 'this is my planned sample size', - 'extra': [] - } - }, - } - }, - }, - 'recommended-analysis': { - 'value': { - 'specify': { - 'value': { - 'question6c': { - 'comments': [], - 'value': 'I used a method of correction for multiple tests', - 'extra': [] - }, - 'question8c': { - 'comments': [], - 'value': 'reliability criteria', - 'extra': [] - }, - 'question9c': { - 'comments': [], - 'value': 'these are the anticipated data transformations', - 'extra': [] - }, - 'question7c': { - 'comments': [], - 'value': 'method of missing data handling', - 'extra': [] - }, - 'question11c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question10c': { - 'comments': [], - 'value': 'assumptions of analysses', - 'extra': [] - } - }, - } - }, - }, - 'description-hypothesis': { - 'value': { - 'question2a': { - 'comments': [], - 'value': 'expected interaction shape', - 'extra': [] - }, - 'question1a': { - 'comments': [], - 'value': 'These are the essential elements', - 'extra': [] - }, - 'question3a': { - 'comments': [], - 'value': 'predictions for successful checks', - 'extra': [] - } - }, - }, - 'confirmatory-analyses-first': { - 'value': { - 'first': { - 'value': { - 'question4c': { - 'comments': [], - 'value': 'this the covariate rationale', - 'extra': [] - }, - 'question5c': { - 'comments': [], - 'value': 'these are techniques for null hypo testing', - 'extra': [] - }, - 'question2c': { - 'comments': [], - 'value': 'this is the statistical technicque', - 'extra': [] - }, - 'question3c': { - 'comments': [], - 'value': 'this is each variable role', - 'extra': [] - }, - 'question1c': { - 'comments': [], - 'value': 'these are the relevant variables', - 'extra': [] - } - }, - } - }, - }, - 'confirmatory-analyses-third': { - 'value': { - 'third': { - 'value': { - 'question4c': { - 'comments': [], - 'value': 'here was the rationale', - 'extra': [] - }, - 'question5c': { - 'comments': [], - 'value': 'I used BAYESIAN STATISTICS', - 'extra': [] - }, - 'question2c': { - 'comments': [], - 'value': 't-test', - 'extra': [] - }, - 'question3c': { - 'comments': [], - 'value': 't-test informed covariate', - 'extra': [] - }, - 'question1c': { - 'comments': [], - 'value': '3rd prediction', - 'extra': [] - } - }, - } - }, - }, - 'datacompletion': { - 'comments': [], - 'value': 'No, data collection has not begun', - 'extra': [] - }, - 'looked': { - 'comments': [], - 'value': 'Yes', - 'extra': [] - }, - 'confirmatory-analyses-fourth': { - 'value': { - 'fourth': { - 'value': { - 'question4c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question5c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question2c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question3c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question1c': { - 'comments': [], - 'value': '', - 'extra': [] - } - }, - } - }, - }, - 'confirmatory-analyses-further': { - 'value': { - 'further': { - 'value': { - 'question4c': { - 'comments': [], - 'value': 'this was the rationale', - 'extra': [] - }, - 'question5c': { - 'comments': [], - 'value': 'also Bayesian', - 'extra': [] - }, - 'question2c': { - 'comments': [], - 'value': 'i used a common statistical technique', - 'extra': [] - }, - 'question3c': { - 'comments': [], - 'value': 'this was the independent variable', - 'extra': [] - }, - 'question1c': { - 'comments': [], - 'value': 'FURTHER PREdictions:', - 'extra': [] - } - }, - } - }, - }, - 'recommended-hypothesis': { - 'value': { - 'question5a': { - 'comments': [], - 'value': 'This is the hypotheses that was tested.', - 'extra': [] - }, - 'question4a': { - 'comments': [], - 'value': '', - 'extra': [ - { - 'selectedFileName': 'Alphabet.txt', - 'viewUrl': '/project/85qku/files/osfstorage/5d6d25024d476c088fb8e03b', - 'sha256': 'asdf', - 'nodeId': '85qku', - 'data': { - 'name': 'Alphabet.txt', - }, - }, - { - 'selectedFileName': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'viewUrl': '/project/85qku/files/osfstorage/5d6d25014d476c088fb8e038', - 'sha256': 'asdf', - 'nodeId': '85qku', - 'data': { - 'name': 'Screen Shot 2019-08-30 at 9.04.01 AM.png' - } - } - ] - }, - 'question6a': { - 'comments': [], - 'value': 'this is the outcome that would be predicted by each theory', - 'extra': [] - } - }, - }, - 'additionalComments': { - 'comments': [], - 'value': 'no additional comments', - 'extra': [] - }, - 'confirmatory-analyses-second': { - 'value': { - 'second': { - 'value': { - 'question4c': { - 'comments': [], - 'value': 'here is the rationale', - 'extra': [] - }, - 'question5c': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question2c': { - 'comments': [], - 'value': 'ANOVA test', - 'extra': [] - }, - 'question3c': { - 'comments': [], - 'value': 'it was the covariate', - 'extra': [] - }, - 'question1c': { - 'comments': [], - 'value': 'how 2nd prediction calculated', - 'extra': [] - } - }, - } - }, - }, - 'recommended-methods': { - 'value': { - 'procedure': { - 'value': { - 'question9b-file': { - 'comments': [], - 'value': '', - 'extra': [] - }, - 'question9b': { - 'comments': [], - 'value': 'set fail-safe levels of exclusions', - 'extra': [] - } - }, - } - }, - } -} - -veer_registration_responses = { - 'confirmatory-analyses-third.third.question4c': 'here was the rationale', - 'recommended-hypothesis.question5a': 'This is the hypotheses that was tested.', - 'confirmatory-analyses-further.further.question4c': 'this was the rationale', - 'confirmatory-analyses-fourth.fourth.question5c': '', - 'description-methods.design.question3b': 'These variables are acting as covariates.', - 'confirmatory-analyses-further.further.question2c': 'i used a common statistical technique', - 'description-methods.exclusion-criteria.question8b': 'these are failing check-tests', - 'description-methods.planned-sample.question4b': 'these are the preselection rults', - 'confirmatory-analyses-second.second.question3c': 'it was the covariate', - 'dataCollectionDates': '2020 - 2030', - 'recommended-analysis.specify.question6c': 'I used a method of correction for multiple tests', - 'confirmatory-analyses-first.first.question2c': 'this is the statistical technicque', - 'description-methods.procedure.question10b': 'describe all manipulations', - 'recommended-analysis.specify.question11c': [], - 'recommended-methods.procedure.question9b': 'set fail-safe levels of exclusions', - 'description-hypothesis.question2a': 'expected interaction shape', - 'confirmatory-analyses-second.second.question5c': '', - 'confirmatory-analyses-first.first.question4c': 'this the covariate rationale', - 'description-methods.planned-sample.question6b': 'this is my planned sample size', - 'confirmatory-analyses-third.third.question1c': '3rd prediction', - 'confirmatory-analyses-fourth.fourth.question1c': '', - 'recommended-analysis.specify.question9c': 'these are the anticipated data transformations', - 'confirmatory-analyses-fourth.fourth.question2c': '', - 'confirmatory-analyses-third.third.question3c': 't-test informed covariate', - 'recommended-hypothesis.question6a': 'this is the outcome that would be predicted by each theory', - 'confirmatory-analyses-fourth.fourth.question4c': '', - 'confirmatory-analyses-second.second.question4c': 'here is the rationale', - 'recommended-hypothesis.question4a': [ - { - 'file_name': 'Alphabet.txt', - 'file_id': '5d6d25024d476c088fb8e03b', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/85qku/files/osfstorage/5d6d25024d476c088fb8e03b'), - }, - 'file_hashes': { - 'sha256': 'asdf', - }, - }, - { - 'file_name': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'file_id': '5d6d25014d476c088fb8e038', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/85qku/files/osfstorage/5d6d25014d476c088fb8e038'), - }, - 'file_hashes': { - 'sha256': 'asdf', - }, - } - ], - 'confirmatory-analyses-third.third.question5c': 'I used BAYESIAN STATISTICS', - 'description-methods.design.question2b': 'these are my dependent variables', - 'description-methods.design.question2a': 'a. whether they are between participants', - 'datacompletion': 'No, data collection has not begun', - 'description-methods.planned-sample.question5b': 'here is how the data will be collected', - 'confirmatory-analyses-further.further.question3c': 'this was the independent variable', - 'confirmatory-analyses-further.further.question1c': 'FURTHER PREdictions:', - 'confirmatory-analyses-second.second.question2c': 'ANOVA test', - 'additionalComments': 'no additional comments', - 'confirmatory-analyses-first.first.question1c': 'these are the relevant variables', - 'recommended-analysis.specify.question7c': 'method of missing data handling', - 'confirmatory-analyses-first.first.question3c': 'this is each variable role', - 'description-hypothesis.question3a': 'predictions for successful checks', - 'confirmatory-analyses-further.further.question5c': 'also Bayesian', - 'recommended-analysis.specify.question10c': 'assumptions of analysses', - 'recommended-methods.procedure.question9b-file': [], - 'description-hypothesis.question1a': 'These are the essential elements', - 'description-methods.planned-sample.question7b': 'here is my data collection termination rule', - 'confirmatory-analyses-first.first.question5c': 'these are techniques for null hypo testing', - 'looked': 'Yes', - 'confirmatory-analyses-second.second.question1c': 'how 2nd prediction calculated', - 'confirmatory-analyses-third.third.question2c': 't-test', - 'confirmatory-analyses-fourth.fourth.question3c': '', - 'recommended-analysis.specify.question8c': 'reliability criteria', - 'description-methods.planned-sample.question6b-upload': [] -} - - -@pytest.fixture() -def osf_standard_schema(): - return RegistrationSchema.objects.get( - name='OSF-Standard Pre-Data Collection Registration', - schema_version=2 - ) - -@pytest.fixture() -def prereg_schema(): - return RegistrationSchema.objects.get( - name='Prereg Challenge', - schema_version=2 - ) - -@pytest.fixture() -def veer_schema(): - return RegistrationSchema.objects.get( - name__icontains='Pre-Registration in Social Psychology', - schema_version=2 - ) - - -@pytest.mark.django_db -class TestMigrateDraftRegistrationRegistrationResponses: - - @pytest.fixture() - def draft_osf_standard(self, osf_standard_schema): - draft = DraftRegistrationFactory( - registration_schema=osf_standard_schema, - registration_metadata={ - 'looked': { - 'comments': [], - 'value': 'Yes', - 'extra': [] - }, - 'datacompletion': { - 'comments': [], - 'value': 'No, data collection has not begun', - 'extra': [] - }, - 'comments': { - 'comments': [], - 'value': 'more comments', - 'extra': [] - } - } - ) - draft.registration_responses = {} - draft.save() - return draft - - @pytest.fixture() - def empty_draft_osf_standard(self, osf_standard_schema): - draft = DraftRegistrationFactory( - registration_schema=osf_standard_schema, - registration_metadata={} - ) - draft.registration_responses = {} - draft.registration_responses_migrated = False - draft.save() - return draft - - @pytest.fixture() - def draft_prereg(self, prereg_schema): - draft = DraftRegistrationFactory( - registration_schema=prereg_schema, - registration_metadata=prereg_registration_metadata - ) - draft.registration_responses = {} - draft.registration_responses_migrated = False - draft.save() - return draft - - @pytest.fixture() - def draft_veer(self, veer_schema): - draft = DraftRegistrationFactory( - registration_schema=veer_schema, - registration_metadata=veer_registration_metadata - ) - draft.registration_responses = {} - draft.registration_responses_migrated = False - draft.save() - return draft - - def test_migrate_empty_draft(self, app, empty_draft_osf_standard): - assert empty_draft_osf_standard.registration_responses == {} - assert empty_draft_osf_standard.registration_responses_migrated is False - - migrate_draft_registrations(dry_run=False) - - empty_draft_osf_standard.reload() - assert empty_draft_osf_standard.registration_responses_migrated is True - responses = empty_draft_osf_standard.registration_responses - assert responses['looked'] == '' - assert responses['datacompletion'] == '' - assert responses['comments'] == '' - - def test_migrate_draft_registrations(self, app, draft_osf_standard, draft_prereg, draft_veer): - drafts = [ - draft_osf_standard, - draft_prereg, - draft_veer - ] - - for draft in drafts: - assert draft.registration_responses == {} - draft.registration_responses_migrated = False - draft.save() - - migrate_draft_registrations(dry_run=False) - - # Verifying OSF Standard Migration - draft_osf_standard.reload() - assert draft_osf_standard.registration_responses_migrated is True - responses = draft_osf_standard.registration_responses - assert responses['looked'] == 'Yes' - assert responses['datacompletion'] == 'No, data collection has not begun' - assert responses['comments'] == 'more comments' - - # Verifying Prereg Migration - draft_prereg.reload() - assert draft_prereg.registration_responses_migrated is True - responses = draft_prereg.registration_responses - assert responses['q12.uploader'] == [] - assert responses['q7.question'] == 'data collection procedures' - assert responses['q21'] == 'research plan follow up' - assert responses['q22'] == 'criteria' - assert responses['q23'] == 'this is how outliers will be handled' - assert responses['q24'] == 'this is how I will deal with incomplete data.' - assert responses['q25'] == 'this is my exploratory analysis' - assert responses['q26'] == [] - assert responses['q27'] == 'No additional comments' - assert responses['q12.question'] == 'these are my measured variables' - assert responses['q1'] == 'This is my title' - assert responses['q3'] == 'research questions' - assert responses['q5'] == 'Registration prior to creation of data' - assert responses['q4'] == 'this is my hypothesis' - assert responses['q6'] == 'Explanation of existing data' - assert responses['q9'] == 'this is the rationale for my sample size' - assert responses['q8'] == 'this is my sample size' - assert responses['q13.question'] == 'these are my indices' - assert responses['q19.uploader'] == [] - assert responses['q11.uploader'] == [ - { - 'file_name': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'file_id': '5d6d22264d476c088fb8e01f', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22264d476c088fb8e01f'), - 'download': urljoin(settings.DOMAIN, '/download/5d6d22264d476c088fb8e01f'), - }, - 'file_hashes': { - 'sha256': 'sdf', - }, - }, - { - 'file_name': 'Alphabet.txt', - 'file_id': '5d6d22274d476c088fb8e021', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021'), - 'download': urljoin(settings.DOMAIN, '/download/5d6d22274d476c088fb8e021'), - }, - 'file_hashes': { - 'sha256': 'asdf', - }, - } - ] - assert responses['q16.question'] == 'this is my study design' - assert responses['q15'] == [ - 'No blinding is involved in this study.', - 'For studies that involve human subjects, they will not know the treatment group to which they have been assigned.', - 'Research personnel who interact directly with the study subjects (either human or non-human subjects) will not be aware of the assigned treatments.' - ] - assert responses['q14'] == '' - assert responses['q17'] == 'this is my explanation of randomization' - assert responses['q10'] == 'this is my stopping rule' - assert responses['q11.question'] == 'these are my maniuplated variables' - assert responses['q16.uploader'] == [] - assert responses['q19.question'] == 'ANOVA' - assert responses['q13.uploader'] == [] - assert responses['q7.uploader'] == [ - { - 'file_name': 'Alphabet.txt', - 'file_id': '5d6d22274d476c088fb8e021', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021'), - 'download': urljoin(settings.DOMAIN, '/download/5d6d22274d476c088fb8e021'), - }, - 'file_hashes': { - 'sha256': 'dsdfds' - }, - } - ] - - # Verifying Veer Migration - draft_veer.reload() - assert draft_veer.registration_responses_migrated is True - responses = draft_veer.registration_responses - assert responses['confirmatory-analyses-third.third.question4c'] == 'here was the rationale' - assert responses['recommended-hypothesis.question5a'] == 'This is the hypotheses that was tested.' - assert responses['confirmatory-analyses-further.further.question4c'] == 'this was the rationale' - assert responses['confirmatory-analyses-fourth.fourth.question5c'] == '' - assert responses['description-methods.design.question3b'] == 'These variables are acting as covariates.' - assert responses['confirmatory-analyses-second.second.question1c'] == 'how 2nd prediction calculated' - assert responses['description-methods.exclusion-criteria.question8b'] == 'these are failing check-tests' - assert responses['description-methods.planned-sample.question4b'] == 'these are the preselection rults' - assert responses['confirmatory-analyses-second.second.question3c'] == 'it was the covariate' - assert responses['recommended-analysis.specify.question6c'] == 'I used a method of correction for multiple tests' - assert responses['confirmatory-analyses-first.first.question2c'] == 'this is the statistical technicque' - assert responses['description-methods.procedure.question10b'] == 'describe all manipulations' - assert responses['recommended-analysis.specify.question11c'] == [] - assert responses['recommended-methods.procedure.question9b'] == 'set fail-safe levels of exclusions' - assert responses['description-hypothesis.question2a'] == 'expected interaction shape' - assert responses['confirmatory-analyses-second.second.question5c'] == '' - assert responses['confirmatory-analyses-first.first.question4c'] == 'this the covariate rationale' - assert responses['description-methods.planned-sample.question6b'] == 'this is my planned sample size' - assert responses['confirmatory-analyses-third.third.question1c'] == '3rd prediction' - assert responses['recommended-analysis.specify.question9c'] == 'these are the anticipated data transformations' - assert responses['confirmatory-analyses-fourth.fourth.question2c'] == '' - assert responses['confirmatory-analyses-third.third.question3c'] == 't-test informed covariate' - assert responses['recommended-hypothesis.question6a'] == 'this is the outcome that would be predicted by each theory' - assert responses['confirmatory-analyses-fourth.fourth.question4c'] == '' - assert responses['confirmatory-analyses-second.second.question4c'] == 'here is the rationale' - assert responses['recommended-hypothesis.question4a'] == [ - { - 'file_name': 'Alphabet.txt', - 'file_id': '5d6d25024d476c088fb8e03b', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/85qku/files/osfstorage/5d6d25024d476c088fb8e03b'), - 'download': urljoin(settings.DOMAIN, '/download/5d6d25024d476c088fb8e03b'), - }, - 'file_hashes': { - 'sha256': 'asdf', - }, - }, - { - 'file_name': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'file_id': '5d6d25014d476c088fb8e038', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/85qku/files/osfstorage/5d6d25014d476c088fb8e038'), - 'download': urljoin(settings.DOMAIN, '/download/5d6d25014d476c088fb8e038'), - }, - 'file_hashes': { - 'sha256': 'asdf', - }, - } - ] - assert responses['confirmatory-analyses-third.third.question5c'] == 'I used BAYESIAN STATISTICS' - assert responses['description-methods.design.question2b'] == 'these are my dependent variables' - assert responses['description-methods.design.question2a'] == 'a. whether they are between participants' - assert responses['datacompletion'] == 'No, data collection has not begun' - assert responses['description-methods.planned-sample.question5b'] == 'here is how the data will be collected' - assert responses['confirmatory-analyses-further.further.question3c'] == 'this was the independent variable' - assert responses['confirmatory-analyses-further.further.question1c'] == 'FURTHER PREdictions:' - assert responses['confirmatory-analyses-second.second.question2c'] == 'ANOVA test' - assert responses['additionalComments'] == 'no additional comments' - assert responses['looked'] == 'Yes' - assert responses['confirmatory-analyses-first.first.question1c'] == 'these are the relevant variables' - assert responses['recommended-analysis.specify.question7c'] == 'method of missing data handling' - assert responses['confirmatory-analyses-first.first.question3c'] == 'this is each variable role' - assert responses['description-hypothesis.question3a'] == 'predictions for successful checks' - assert responses['confirmatory-analyses-further.further.question2c'] == 'i used a common statistical technique' - assert responses['confirmatory-analyses-further.further.question5c'] == 'also Bayesian' - assert responses['recommended-analysis.specify.question10c'] == 'assumptions of analysses' - assert responses['recommended-methods.procedure.question9b-file'] == [] - assert responses['description-hypothesis.question1a'] == 'These are the essential elements' - assert responses['description-methods.planned-sample.question7b'] == 'here is my data collection termination rule' - assert responses['confirmatory-analyses-first.first.question5c'] == 'these are techniques for null hypo testing' - assert responses['dataCollectionDates'] == '2020 - 2030' - assert responses['confirmatory-analyses-fourth.fourth.question1c'] == '' - assert responses['confirmatory-analyses-third.third.question2c'] == 't-test' - assert responses['confirmatory-analyses-fourth.fourth.question3c'] == '' - assert responses['recommended-analysis.specify.question8c'] == 'reliability criteria' - assert responses['description-methods.planned-sample.question6b-upload'] == [] - - -@pytest.mark.django_db -class TestMigrateRegistrationRegistrationResponses: - - @pytest.fixture() - def reg_osf_standard(self, osf_standard_schema): - draft = DraftRegistrationFactory( - registration_schema=osf_standard_schema, - registration_metadata={ - 'looked': { - 'comments': [], - 'value': 'Yes', - 'extra': [] - }, - 'datacompletion': { - 'comments': [], - 'value': 'No, data collection has not begun', - 'extra': [] - }, - 'comments': { - 'comments': [], - 'value': 'more comments', - 'extra': [] - } - } - ) - return RegistrationFactory( - schema=osf_standard_schema, - draft_registration=draft, - project=draft.branched_from - ) - - @pytest.fixture() - def reg_prereg(self, prereg_schema): - draft = DraftRegistrationFactory( - registration_schema=prereg_schema, - registration_metadata=prereg_registration_metadata - ) - return RegistrationFactory( - schema=prereg_schema, - draft_registration=draft, - project=draft.branched_from - ) - - @pytest.fixture() - def reg_veer(self, veer_schema): - draft = DraftRegistrationFactory( - registration_metadata=veer_registration_metadata, - registration_schema=veer_schema, - ) - return RegistrationFactory( - schema=veer_schema, - draft_registration=draft, - project=draft.branched_from - ) - - def test_migrate_registrations(self, app, reg_osf_standard, reg_prereg, reg_veer): - regs = [ - reg_osf_standard, - reg_prereg, - reg_veer - ] - - for reg in regs: - reg.registration_responses = {} - reg.registration_responses_migrated = False - reg.save() - - migrate_registrations(dry_run=False) - - # Verifying OSF Standard Migration - reg_osf_standard.reload() - assert reg_osf_standard.registration_responses_migrated is True - responses = reg_osf_standard.registration_responses - assert responses['looked'] == 'Yes' - assert responses['datacompletion'] == 'No, data collection has not begun' - assert responses['comments'] == 'more comments' - - # Verifying Prereg Migration - reg_prereg.reload() - assert reg_prereg.registration_responses_migrated is True - responses = reg_prereg.registration_responses - assert responses['q12.uploader'] == [] - assert responses['q7.question'] == 'data collection procedures' - assert responses['q21'] == 'research plan follow up' - assert responses['q22'] == 'criteria' - assert responses['q23'] == 'this is how outliers will be handled' - assert responses['q24'] == 'this is how I will deal with incomplete data.' - assert responses['q25'] == 'this is my exploratory analysis' - assert responses['q26'] == [] - assert responses['q27'] == 'No additional comments' - assert responses['q12.question'] == 'these are my measured variables' - assert responses['q1'] == 'This is my title' - assert responses['q3'] == 'research questions' - assert responses['q5'] == 'Registration prior to creation of data' - assert responses['q4'] == 'this is my hypothesis' - assert responses['q6'] == 'Explanation of existing data' - assert responses['q9'] == 'this is the rationale for my sample size' - assert responses['q8'] == 'this is my sample size' - assert responses['q13.question'] == 'these are my indices' - assert responses['q19.uploader'] == [] - assert responses['q11.uploader'] == [ - { - 'file_name': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'file_id': '5d6d22264d476c088fb8e01f', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22264d476c088fb8e01f'), - 'download': urljoin(settings.DOMAIN, '/download/5d6d22264d476c088fb8e01f'), - }, - 'file_hashes': { - 'sha256': 'sdf', - }, - }, - { - 'file_name': 'Alphabet.txt', - 'file_id': '5d6d22274d476c088fb8e021', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021'), - 'download': urljoin(settings.DOMAIN, '/download/5d6d22274d476c088fb8e021'), - }, - 'file_hashes': { - 'sha256': 'asdf', - }, - } - ] - assert responses['q16.question'] == 'this is my study design' - assert responses['q15'] == [ - 'No blinding is involved in this study.', - 'For studies that involve human subjects, they will not know the treatment group to which they have been assigned.', - 'Research personnel who interact directly with the study subjects (either human or non-human subjects) will not be aware of the assigned treatments.' - ] - assert responses['q14'] == '' - assert responses['q17'] == 'this is my explanation of randomization' - assert responses['q10'] == 'this is my stopping rule' - assert responses['q11.question'] == 'these are my maniuplated variables' - assert responses['q16.uploader'] == [] - assert responses['q19.question'] == 'ANOVA' - assert responses['q13.uploader'] == [] - assert responses['q7.uploader'] == [ - { - 'file_name': 'Alphabet.txt', - 'file_id': '5d6d22274d476c088fb8e021', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/57zbh/files/osfstorage/5d6d22274d476c088fb8e021'), - 'download': urljoin(settings.DOMAIN, '/download/5d6d22274d476c088fb8e021'), - }, - 'file_hashes': { - 'sha256': 'dsdfds', - }, - } - ] - - # Verifying Veer Migration - reg_veer.reload() - assert reg_veer.registration_responses_migrated is True - responses = reg_veer.registration_responses - assert responses['confirmatory-analyses-third.third.question4c'] == 'here was the rationale' - assert responses['recommended-hypothesis.question5a'] == 'This is the hypotheses that was tested.' - assert responses['confirmatory-analyses-further.further.question4c'] == 'this was the rationale' - assert responses['confirmatory-analyses-fourth.fourth.question5c'] == '' - assert responses['description-methods.design.question3b'] == 'These variables are acting as covariates.' - assert responses['confirmatory-analyses-second.second.question1c'] == 'how 2nd prediction calculated' - assert responses['description-methods.exclusion-criteria.question8b'] == 'these are failing check-tests' - assert responses['description-methods.planned-sample.question4b'] == 'these are the preselection rults' - assert responses['confirmatory-analyses-second.second.question3c'] == 'it was the covariate' - assert responses['recommended-analysis.specify.question6c'] == 'I used a method of correction for multiple tests' - assert responses['confirmatory-analyses-first.first.question2c'] == 'this is the statistical technicque' - assert responses['description-methods.procedure.question10b'] == 'describe all manipulations' - assert responses['recommended-analysis.specify.question11c'] == [] - assert responses['recommended-methods.procedure.question9b'] == 'set fail-safe levels of exclusions' - assert responses['description-hypothesis.question2a'] == 'expected interaction shape' - assert responses['confirmatory-analyses-second.second.question5c'] == '' - assert responses['confirmatory-analyses-first.first.question4c'] == 'this the covariate rationale' - assert responses['description-methods.planned-sample.question6b'] == 'this is my planned sample size' - assert responses['confirmatory-analyses-third.third.question1c'] == '3rd prediction' - assert responses['recommended-analysis.specify.question9c'] == 'these are the anticipated data transformations' - assert responses['confirmatory-analyses-fourth.fourth.question2c'] == '' - assert responses['confirmatory-analyses-third.third.question3c'] == 't-test informed covariate' - assert responses['recommended-hypothesis.question6a'] == 'this is the outcome that would be predicted by each theory' - assert responses['confirmatory-analyses-fourth.fourth.question4c'] == '' - assert responses['confirmatory-analyses-second.second.question4c'] == 'here is the rationale' - assert responses['recommended-hypothesis.question4a'] == [ - { - 'file_name': 'Alphabet.txt', - 'file_id': '5d6d25024d476c088fb8e03b', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/85qku/files/osfstorage/5d6d25024d476c088fb8e03b'), - 'download': urljoin(settings.DOMAIN, '/download/5d6d25024d476c088fb8e03b'), - }, - 'file_hashes': { - 'sha256': 'asdf', - }, - }, - { - 'file_name': 'Screen Shot 2019-08-30 at 9.04.01 AM.png', - 'file_id': '5d6d25014d476c088fb8e038', - 'file_urls': { - 'html': urljoin(settings.DOMAIN, '/project/85qku/files/osfstorage/5d6d25014d476c088fb8e038'), - 'download': urljoin(settings.DOMAIN, '/download/5d6d25014d476c088fb8e038'), - }, - 'file_hashes': { - 'sha256': 'asdf', - }, - } - ] - assert responses['confirmatory-analyses-third.third.question5c'] == 'I used BAYESIAN STATISTICS' - assert responses['description-methods.design.question2b'] == 'these are my dependent variables' - assert responses['description-methods.design.question2a'] == 'a. whether they are between participants' - assert responses['datacompletion'] == 'No, data collection has not begun' - assert responses['description-methods.planned-sample.question5b'] == 'here is how the data will be collected' - assert responses['confirmatory-analyses-further.further.question3c'] == 'this was the independent variable' - assert responses['confirmatory-analyses-further.further.question1c'] == 'FURTHER PREdictions:' - assert responses['confirmatory-analyses-second.second.question2c'] == 'ANOVA test' - assert responses['additionalComments'] == 'no additional comments' - assert responses['looked'] == 'Yes' - assert responses['confirmatory-analyses-first.first.question1c'] == 'these are the relevant variables' - assert responses['recommended-analysis.specify.question7c'] == 'method of missing data handling' - assert responses['confirmatory-analyses-first.first.question3c'] == 'this is each variable role' - assert responses['description-hypothesis.question3a'] == 'predictions for successful checks' - assert responses['confirmatory-analyses-further.further.question2c'] == 'i used a common statistical technique' - assert responses['confirmatory-analyses-further.further.question5c'] == 'also Bayesian' - assert responses['recommended-analysis.specify.question10c'] == 'assumptions of analysses' - assert responses['recommended-methods.procedure.question9b-file'] == [] - assert responses['description-hypothesis.question1a'] == 'These are the essential elements' - assert responses['description-methods.planned-sample.question7b'] == 'here is my data collection termination rule' - assert responses['confirmatory-analyses-first.first.question5c'] == 'these are techniques for null hypo testing' - assert responses['dataCollectionDates'] == '2020 - 2030' - assert responses['confirmatory-analyses-fourth.fourth.question1c'] == '' - assert responses['confirmatory-analyses-third.third.question2c'] == 't-test' - assert responses['confirmatory-analyses-fourth.fourth.question3c'] == '' - assert responses['recommended-analysis.specify.question8c'] == 'reliability criteria' - assert responses['description-methods.planned-sample.question6b-upload'] == [] diff --git a/osf_tests/test_archiver.py b/osf_tests/test_archiver.py index 3855d169acb..70bd476f9e2 100644 --- a/osf_tests/test_archiver.py +++ b/osf_tests/test_archiver.py @@ -656,8 +656,8 @@ def test_archive_success_different_name_same_sha(self): with mock.patch.object(BaseStorageAddon, '_get_file_tree', mock.Mock(return_value=file_tree)): job = factories.ArchiveJobFactory(initiator=registration.creator) archive_success(registration._id, job._id) - for key, question in registration.registered_meta[schema._id].items(): - assert question['extra'][0]['selectedFileName'] == fake_file['name'] + # for key, question in registration.registered_meta[schema._id].items(): + # assert question['extra'][0]['selectedFileName'] == fake_file['name'] def test_archive_failure_different_name_same_sha(self): file_tree = file_tree_factory(0, 0, 0) @@ -712,8 +712,8 @@ def test_archive_success_same_file_in_component(self): archive_success(registration._id, job._id) registration.reload() child_reg = registration.nodes[0] - for key, question in registration.registered_meta[schema._id].items(): - assert child_reg._id in question['extra'][0]['viewUrl'] + for rr in registration.registered_responses.all(): + assert child_reg._id in rr.url # rr question['extra'][0]['viewUrl'] class TestArchiverUtils(ArchiverTestCase): diff --git a/osf_tests/test_draft_registration.py b/osf_tests/test_draft_registration.py index 225719efdd4..9f8f51177fc 100644 --- a/osf_tests/test_draft_registration.py +++ b/osf_tests/test_draft_registration.py @@ -92,40 +92,6 @@ def test_register_no_title_fails(self): assert str(e.value) == 'Draft Registration must have title to be registered' - def test_update_metadata_updates_registration_responses(self, project): - schema = RegistrationSchema.objects.get( - name='OSF-Standard Pre-Data Collection Registration', - schema_version=2 - ) - draft = factories.DraftRegistrationFactory(registration_schema=schema, branched_from=project) - new_metadata = { - 'looked': { - 'comments': [], - 'value': 'Yes', - 'extra': [] - }, - 'datacompletion': { - 'comments': [], - 'value': 'No, data collection has not begun', - 'extra': [] - }, - 'comments': { - 'comments': [], - 'value': '', - 'extra': [] - } - } - draft.update_metadata(new_metadata) - draft.save() - # To preserve both workflows, if update_metadata is called, - # a flattened version of that metadata is stored in - # registration_responses - assert draft.registration_responses == { - 'looked': 'Yes', - 'datacompletion': 'No, data collection has not begun', - 'comments': '' - } - def test_update_registration_responses(self, project): schema = RegistrationSchema.objects.get( name='OSF-Standard Pre-Data Collection Registration', diff --git a/osf_tests/test_node.py b/osf_tests/test_node.py index 4e88d1c2bf7..b409561ac84 100644 --- a/osf_tests/test_node.py +++ b/osf_tests/test_node.py @@ -711,7 +711,6 @@ def test_project_factory(self): assert hasattr(node, 'forked_date') assert bool(node.title) assert hasattr(node, 'description') - assert hasattr(node, 'registered_meta') assert hasattr(node, 'registered_user') assert hasattr(node, 'registered_schema') assert bool(node.creator) @@ -2003,7 +2002,6 @@ def test_register_node_propagates_schema_and_data_to_children(self, mock_signal, r1 = reg.nodes[0] r1a = r1.nodes[0] for r in [reg, r1, r1a]: - assert r.registered_meta[meta_schema._id] == data assert r.registration_responses == expected_flat_data assert r.registered_schema.first() == meta_schema @@ -2101,7 +2099,6 @@ def test_register_node_contributor_questions(self, mock_signal, user, auth): # so there aren't discrepancies # assert that other registration_metadata not overridden - assert registration.registered_meta[registration.registration_schema._id]['q3']['value'] == 'research questions' assert registration.registration_responses['q3'] == 'research questions' diff --git a/osf_tests/test_registrations.py b/osf_tests/test_registrations.py index 970b3670a3e..ef3e9742168 100644 --- a/osf_tests/test_registrations.py +++ b/osf_tests/test_registrations.py @@ -20,12 +20,6 @@ from api.providers.workflows import Workflows from osf.migrations import update_provider_auth_groups from osf.models.action import RegistrationAction -from osf_tests.management_commands.test_migration_registration_responses import ( - prereg_registration_responses, - prereg_registration_metadata_built, - veer_registration_responses, - veer_condensed -) from osf.utils.workflows import ( RegistrationModerationStates, RegistrationModerationTriggers, @@ -615,25 +609,6 @@ def veer_schema(self): schema_version=2 ) - def test_expand_registration_responses(self, draft_prereg): - draft_prereg.registration_responses = prereg_registration_responses - draft_prereg.save() - assert draft_prereg.registration_metadata == {} - - registration_metadata = draft_prereg.expand_registration_responses() - - assert registration_metadata == prereg_registration_metadata_built - - def test_expand_registration_responses_veer(self, draft_veer): - draft_veer.registration_responses = veer_registration_responses - draft_veer.save() - assert draft_veer.registration_metadata == {} - - registration_metadata = draft_veer.expand_registration_responses() - - assert registration_metadata == veer_condensed - - class TestRegistationModerationStates: @pytest.fixture diff --git a/website/archiver/utils.py b/website/archiver/utils.py index 44cd7517413..60d2c093871 100644 --- a/website/archiver/utils.py +++ b/website/archiver/utils.py @@ -265,7 +265,6 @@ def migrate_file_metadata(dst): response_block.set_response(updated_response) dst.registration_responses = dst.schema_responses.get().all_responses - dst.registered_meta[schema._id] = dst.expand_registration_responses() dst.save() diff --git a/website/project/views/drafts.py b/website/project/views/drafts.py index e4ee762c992..d477f8f988d 100644 --- a/website/project/views/drafts.py +++ b/website/project/views/drafts.py @@ -1,22 +1,18 @@ import functools from rest_framework import status as http_status import itertools -import html from operator import itemgetter from dateutil.parser import parse as parse_date from django.utils import timezone -from flask import request, redirect +from flask import request import pytz from framework.database import autoload from framework.exceptions import HTTPError -from osf import features -from osf.utils.sanitize import strip_html from osf.utils.permissions import ADMIN -from osf.utils.functional import rapply from osf.models import RegistrationSchema, DraftRegistration from website.project.decorators import ( @@ -25,24 +21,13 @@ must_have_permission, ) from website import settings -from website.ember_osf_web.decorators import ember_flag_is_active -from website.project import utils from website.project.metadata.schemas import METASCHEMA_ORDERING from website.project.metadata.utils import serialize_meta_schema, serialize_draft_registration from website.project.utils import serialize_node autoload_draft = functools.partial(autoload, DraftRegistration, 'draft_id', 'draft') -def get_schema_or_fail(schema_name, schema_version): - try: - meta_schema = RegistrationSchema.objects.get(name=schema_name, schema_version=schema_version) - except RegistrationSchema.DoesNotExist: - raise HTTPError(http_status.HTTP_404_NOT_FOUND, data=dict( - message_long='No RegistrationSchema record matching that query could be found' - )) - return meta_schema - def must_be_branched_from_node(func): @autoload_draft @must_be_valid_project @@ -150,95 +135,6 @@ def get_draft_registrations(auth, node, *args, **kwargs): 'drafts': sorted_serialized_drafts }, http_status.HTTP_200_OK -@must_have_permission(ADMIN) -@must_be_valid_project -@must_be_contributor_and_not_group_member -@ember_flag_is_active(features.EMBER_CREATE_DRAFT_REGISTRATION) -def new_draft_registration(auth, node, *args, **kwargs): - """Create a new draft registration for the node - - :return: Redirect to the new draft's edit page - :rtype: flask.redirect - :raises: HTTPError - """ - if node.is_registration: - raise HTTPError(http_status.HTTP_403_FORBIDDEN, data={ - 'message_short': "Can't create draft", - 'message_long': 'Creating draft registrations on registered projects is not allowed.' - }) - data = request.values - - schema_name = data.get('schema_name') - if not schema_name: - raise HTTPError( - http_status.HTTP_400_BAD_REQUEST, - data={ - 'message_short': 'Must specify a schema_name', - 'message_long': 'Please specify a schema_name' - } - ) - - schema_version = data.get('schema_version', 2) - - meta_schema = get_schema_or_fail(schema_name, int(schema_version)) - draft = DraftRegistration.create_from_node( - node=node, - user=auth.user, - schema=meta_schema, - data={} - ) - return redirect(node.web_url_for('edit_draft_registration_page', draft_id=draft._id, _guid=True)) - - -@must_have_permission(ADMIN) -@must_be_contributor_and_not_group_member -@ember_flag_is_active(features.EMBER_EDIT_DRAFT_REGISTRATION) -@must_be_branched_from_node -def edit_draft_registration_page(auth, node, draft, **kwargs): - """Draft registration editor - - :return: serialized DraftRegistration - :rtype: dict - """ - check_draft_state(draft) - ret = utils.serialize_node(node, auth, primary=True) - ret['draft'] = serialize_draft_registration(draft, auth) - return ret - -@must_have_permission(ADMIN) -@must_be_contributor_and_not_group_member -@must_be_branched_from_node -def update_draft_registration(auth, node, draft, *args, **kwargs): - """Update an existing draft registration - - :return: serialized draft registration - :rtype: dict - :raises: HTTPError - """ - check_draft_state(draft) - data = request.get_json() - - schema_data = data.get('schema_data', {}) - schema_data = rapply(schema_data, strip_html) - - # Unencodes HTML special characters in filenames - if schema_data.get('uploader', {}).get('value'): - schema_data['uploader']['value'] = html.unescape(schema_data['uploader']['value']) - if schema_data.get('uploader', {}).get('extra'): - for extra in schema_data['uploader']['extra']: - extra['selectedFileName'] = html.unescape(extra['selectedFileName']) - - schema_name = data.get('schema_name') - schema_version = data.get('schema_version', 1) - if schema_name: - meta_schema = get_schema_or_fail(schema_name, schema_version) - existing_schema = draft.registration_schema - if (existing_schema.name, existing_schema.schema_version) != (meta_schema.name, meta_schema.schema_version): - draft.registration_schema = meta_schema - - draft.update_metadata(schema_data) - draft.save() - return serialize_draft_registration(draft, auth), http_status.HTTP_200_OK @must_have_permission(ADMIN) @must_be_contributor_and_not_group_member diff --git a/website/project/views/node.py b/website/project/views/node.py index 9bad3713d3b..5a10ef00853 100644 --- a/website/project/views/node.py +++ b/website/project/views/node.py @@ -24,7 +24,6 @@ from osf.models.nodelog import NodeLog from osf.models.user import OSFUser from osf.utils.functional import rapply -from osf.utils.registrations import strip_registered_meta_comments from osf.utils import sanitize from osf import features @@ -805,7 +804,6 @@ def _view_project(node, auth, primary=False, 'registered_from_url': node.registered_from.url if is_registration else '', 'registered_date': iso8601format(node.registered_date) if is_registration else '', 'root_id': node.root._id if node.root else None, - 'registered_meta': strip_registered_meta_comments(node.registered_meta), 'registered_schemas': serialize_meta_schemas( list(node.registered_schema.all())) if is_registration else False, 'is_fork': node.is_fork, diff --git a/website/routes.py b/website/routes.py index 227d68302e3..51e5597238e 100644 --- a/website/routes.py +++ b/website/routes.py @@ -1271,22 +1271,6 @@ def make_url_map(app): project_views.node.node_registrations, notemplate, ), - Rule( - [ - '/project//registrations/', - '/project//node//registrations/', - ], - 'post', - project_views.drafts.new_draft_registration, - OsfWebRenderer('project/edit_draft_registration.mako', trust=False)), - Rule( - [ - '/project//drafts//', - '/project//node//drafts//', - ], - 'get', - project_views.drafts.edit_draft_registration_page, - OsfWebRenderer('project/edit_draft_registration.mako', trust=False)), Rule( [ '/project//drafts//register/', @@ -1527,9 +1511,6 @@ def make_url_map(app): Rule([ '/project//drafts//', ], 'get', project_views.drafts.get_draft_registration, json_renderer), - Rule([ - '/project//drafts//', - ], 'put', project_views.drafts.update_draft_registration, json_renderer), Rule([ '/project//drafts//', ], 'delete', project_views.drafts.delete_draft_registration, json_renderer), From 582e271138a08d49278c70c56833c30c66acd756 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Fri, 11 Apr 2025 10:21:59 -0400 Subject: [PATCH 9/9] fix migrations --- ..._more.py => 0030_remove_abstractnode_registered_metadata.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osf/migrations/{0029_remove_abstractnode_registered_meta_and_more.py => 0030_remove_abstractnode_registered_metadata.py} (86%) diff --git a/osf/migrations/0029_remove_abstractnode_registered_meta_and_more.py b/osf/migrations/0030_remove_abstractnode_registered_metadata.py similarity index 86% rename from osf/migrations/0029_remove_abstractnode_registered_meta_and_more.py rename to osf/migrations/0030_remove_abstractnode_registered_metadata.py index 541d21d3907..ac3e29948d9 100644 --- a/osf/migrations/0029_remove_abstractnode_registered_meta_and_more.py +++ b/osf/migrations/0030_remove_abstractnode_registered_metadata.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ('osf', '0028_collection_grade_levels_choices_and_more'), + ('osf', '0029_remove_abstractnode_keenio_read_key'), ] operations = [