diff --git a/api/base/filters.py b/api/base/filters.py index 7a5e6ed2450..897144a8f8b 100644 --- a/api/base/filters.py +++ b/api/base/filters.py @@ -114,26 +114,6 @@ def remove_invalid_fields(self, queryset, fields, view, request): return valid_fields -class ElasticOSFOrderingFilter(OSFOrderingFilter): - """ This is too enable sorting for ES endpoints that use ES results instead of a typical queryset""" - def filter_queryset(self, request, queryset, view): - sorted_list = queryset.copy() - sort = request.query_params.get('sort') - reverse = False - if sort: - if sort.startswith('-'): - sort = sort.lstrip('-') - reverse = True - - try: - source = view.get_serializer_class()._declared_fields[sort].source - sorted_list['results'] = sorted(queryset['results'], key=lambda item: item['_source'][source], reverse=reverse) - except KeyError: - pass - - return sorted_list - - class FilterMixin: """ View mixin with helper functions for filtering. """ diff --git a/api/base/pagination.py b/api/base/pagination.py index f26e54bfbad..25fba6c4abe 100644 --- a/api/base/pagination.py +++ b/api/base/pagination.py @@ -11,10 +11,8 @@ ) from api.base.serializers import is_anonymized from api.base.settings import MAX_PAGE_SIZE, MAX_SIZE_OF_ES_QUERY -from api.base.utils import absolute_reverse from osf.models import AbstractNode, Comment, Preprint, Guid, DraftRegistration -from website.search.elastic_search import DOC_TYPE_TO_MODEL class JSONAPIPagination(pagination.PageNumberPagination): @@ -263,180 +261,3 @@ class DraftRegistrationContributorPagination(NodeContributorPagination): def get_resource(self, kwargs): resource_id = kwargs.get('draft_id') return DraftRegistration.load(resource_id) - - -class SearchPaginator(DjangoPaginator): - - def __init__(self, object_list, per_page): - super().__init__(object_list, per_page) - - def search_type_to_model(self, obj_id, obj_type): - model = DOC_TYPE_TO_MODEL[obj_type] - return model.load(obj_id) - - def _get_count(self): - self._count = self.object_list['aggs']['total'] - return self._count - count = property(_get_count) - - def page(self, number): - number = self.validate_number(number) - results = self.object_list['results'] - items = [ - self.search_type_to_model(result.get('_id'), result.get('_type')) - for result in results - ] - return self._get_page(items, number, self) - - -class SearchModelPaginator(SearchPaginator): - - def __init__(self, object_list, per_page, model): - super().__init__(object_list, per_page) - self.model = model - - def page(self, number): - number = self.validate_number(number) - results = self.object_list['results'] - items = [ - self.model.load(result.get('_id')) - for result in results - ] - return self._get_page(items, number, self) - - -class SearchPagination(JSONAPIPagination): - - def __init__(self): - super().__init__() - self.paginator = None - - def paginate_queryset(self, queryset, request, view=None): - page_size = self.get_page_size(request) - if not page_size: - return None - - # Pagination requires an order by clause, especially when using Postgres. - # see: https://docs.djangoproject.com/en/1.10/topics/pagination/#required-arguments - if isinstance(queryset, QuerySet) and not queryset.ordered: - queryset = queryset.order_by(queryset.model._meta.pk.name) - - self.paginator = SearchPaginator(queryset, page_size) - model = getattr(request.parser_context['view'], 'model_class', None) - if model: - self.paginator = SearchModelPaginator(queryset, page_size, model) - - page_number = request.query_params.get(self.page_query_param, 1) - if page_number in self.last_page_strings: - page_number = self.paginator.num_pages - - try: - self.page = self.paginator.page(page_number) - except InvalidPage as exc: - msg = self.invalid_page_message.format( - page_number=page_number, message=str(exc), - ) - raise NotFound(msg) - - if self.paginator.num_pages > 1 and self.template is not None: - # The browsable API should display pagination controls. - self.display_page_controls = True - - self.request = request - return list(self.page) - - def get_search_field_url(self, field, query): - view_name = f'search:search-{field}' - return absolute_reverse( - view_name, - query_kwargs={ - 'q': query, - }, - kwargs={ - 'version': self.request.parser_context['kwargs']['version'], - }, - ) - - def get_search_field_total(self, field): - return self.paginator.object_list['counts'].get(field, 0) - - def get_search_field(self, field, query): - return OrderedDict([ - ( - 'related', OrderedDict([ - ('href', self.get_search_field_url(field, query)), - ( - 'meta', OrderedDict([ - ('total', self.get_search_field_total(field)), - ]), - ), - ]), - ), - ]) - - def get_response_dict(self, data, url): - if isinstance(self.paginator, SearchModelPaginator): - return super().get_response_dict(data, url) - else: - query = self.request.query_params.get('q', '*') - return OrderedDict([ - ('data', data), - ( - 'search_fields', OrderedDict([ - ('files', self.get_search_field('file', query)), - ('projects', self.get_search_field('project', query)), - ('components', self.get_search_field('component', query)), - ('registrations', self.get_search_field('registration', query)), - ('users', self.get_search_field('user', query)), - ('institutions', self.get_search_field('institution', query)), - ]), - ), - ( - 'meta', OrderedDict([ - ('total', self.page.paginator.count), - ('per_page', self.page.paginator.per_page), - ]), - ), - ( - 'links', OrderedDict([ - ('self', self.get_self_real_link(url)), - ('first', self.get_first_real_link(url)), - ('last', self.get_last_real_link(url)), - ('prev', self.get_previous_real_link(url)), - ('next', self.get_next_real_link(url)), - ]), - ), - ]) - - def get_response_dict_deprecated(self, data, url): - if isinstance(self.paginator, SearchModelPaginator): - return super().get_response_dict_deprecated(data, url) - else: - query = self.request.query_params.get('q', '*') - return OrderedDict([ - ('data', data), - ( - 'search_fields', OrderedDict([ - ('files', self.get_search_field('file', query)), - ('projects', self.get_search_field('project', query)), - ('components', self.get_search_field('component', query)), - ('registrations', self.get_search_field('registration', query)), - ('users', self.get_search_field('user', query)), - ('institutions', self.get_search_field('institution', query)), - ]), - ), - ( - 'links', OrderedDict([ - ('first', self.get_first_real_link(url)), - ('last', self.get_last_real_link(url)), - ('prev', self.get_previous_real_link(url)), - ('next', self.get_next_real_link(url)), - ( - 'meta', OrderedDict([ - ('total', self.page.paginator.count), - ('per_page', self.page.paginator.per_page), - ]), - ), - ]), - ), - ]) diff --git a/api/base/parsers.py b/api/base/parsers.py index d0ce368e6a5..11bff6b774c 100644 --- a/api/base/parsers.py +++ b/api/base/parsers.py @@ -3,7 +3,6 @@ import time import collections from jsonschema import validate, ValidationError, Draft7Validator -from django.core.exceptions import ImproperlyConfigured from rest_framework.parsers import JSONParser from rest_framework.exceptions import ParseError, NotAuthenticated @@ -307,52 +306,3 @@ def parse(self, stream, media_type=None, parser_context=None): raise JSONAPIException(detail='Signature has expired') return payload - -class SearchParser(JSONAPIParser): - - def parse(self, stream, media_type=None, parser_context=None): - try: - view = parser_context['view'] - except KeyError: - raise ImproperlyConfigured('SearchParser requires "view" context.') - data = super().parse(stream, media_type=media_type, parser_context=parser_context) - if not data: - raise JSONAPIException(detail='Invalid Payload') - - res = { - 'query': { - 'bool': {}, - }, - } - - sort = parser_context['request'].query_params.get('sort') - if sort: - res['sort'] = [{ - sort.lstrip('-'): { - 'order': 'desc' if sort.startswith('-') else 'asc', - }, - }] - - try: - q = data.pop('q') - except KeyError: - pass - else: - res['query']['bool'].update({ - 'must': { - 'query_string': { - 'query': q, - 'fields': view.search_fields, - }, - }, - }) - - if any(data.values()): - res['query']['bool'].update({'filter': []}) - for key, val in data.items(): - if val is not None: - if isinstance(val, list): - res['query']['bool']['filter'].append({'terms': {key: val}}) - else: - res['query']['bool']['filter'].append({'term': {key: val}}) - return res diff --git a/api/base/urls.py b/api/base/urls.py index 142e2df34c2..63984efd287 100644 --- a/api/base/urls.py +++ b/api/base/urls.py @@ -70,7 +70,6 @@ re_path(r'^requests/', include(('api.requests.urls', 'requests'), namespace='requests')), re_path(r'^resources/', include('api.resources.urls', namespace='resources')), re_path(r'^scopes/', include('api.scopes.urls', namespace='scopes')), - re_path(r'^search/', include('api.search.urls', namespace='search')), re_path(r'^sparse/', include('api.sparse.urls', namespace='sparse')), re_path(r'^subjects/', include('api.subjects.urls', namespace='subjects')), re_path(r'^subscriptions/', include('api.subscriptions.urls', namespace='subscriptions')), diff --git a/api/search/__init__.py b/api/search/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api/search/permissions.py b/api/search/permissions.py deleted file mode 100644 index 7266e211d75..00000000000 --- a/api/search/permissions.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework.permissions import IsAuthenticatedOrReadOnly - -class IsAuthenticatedOrReadOnlyForSearch(IsAuthenticatedOrReadOnly): - def has_permission(self, request, view): - from api.search.views import BaseSearchView - if not isinstance(view, BaseSearchView): - return False - return request.method == 'POST' or super().has_permission(request, view) diff --git a/api/search/serializers.py b/api/search/serializers.py deleted file mode 100644 index 6d07a131856..00000000000 --- a/api/search/serializers.py +++ /dev/null @@ -1,58 +0,0 @@ -from api.base.serializers import ( - JSONAPISerializer, -) -from api.base.utils import absolute_reverse -from api.files.serializers import FileSerializer -from api.nodes.serializers import NodeSerializer -from api.registrations.serializers import RegistrationSerializer -from api.users.serializers import UserSerializer -from api.institutions.serializers import InstitutionSerializer -from api.collections.serializers import CollectionSubmissionSerializer - -from osf.models import ( - AbstractNode, - OSFUser, - BaseFileNode, - Institution, - CollectionSubmission, -) - -class SearchSerializer(JSONAPISerializer): - - def to_representation(self, data, envelope='data'): - - if isinstance(data, AbstractNode): - if data.is_registration: - serializer = RegistrationSerializer(data, context=self.context) - return RegistrationSerializer.to_representation(serializer, data) - serializer = NodeSerializer(data, context=self.context) - return NodeSerializer.to_representation(serializer, data) - - if isinstance(data, OSFUser): - serializer = UserSerializer(data, context=self.context) - return UserSerializer.to_representation(serializer, data) - - if isinstance(data, BaseFileNode): - serializer = FileSerializer(data, context=self.context) - return FileSerializer.to_representation(serializer, data) - - if isinstance(data, Institution): - serializer = InstitutionSerializer(data, context=self.context) - return InstitutionSerializer.to_representation(serializer, data) - - if isinstance(data, CollectionSubmission): - serializer = CollectionSubmissionSerializer(data, context=self.context) - return CollectionSubmissionSerializer.to_representation(serializer, data) - - return None - - def get_absolute_url(self, obj): - return absolute_reverse( - view_name='search:search-search', - kwargs={ - 'version': self.context['request'].parser_context['kwargs']['version'], - }, - ) - - class Meta: - type_ = 'search' diff --git a/api/search/urls.py b/api/search/urls.py deleted file mode 100644 index 3375e0eb8a6..00000000000 --- a/api/search/urls.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.urls import re_path - -from api.search import views - -app_name = 'osf' - -urlpatterns = [ - re_path(r'^$', views.Search.as_view(), name=views.Search.view_name), - re_path(r'^components/$', views.SearchComponents.as_view(), name=views.SearchComponents.view_name), - re_path(r'^files/$', views.SearchFiles.as_view(), name=views.SearchFiles.view_name), - re_path(r'^projects/$', views.SearchProjects.as_view(), name=views.SearchProjects.view_name), - re_path(r'^registrations/$', views.SearchRegistrations.as_view(), name=views.SearchRegistrations.view_name), - re_path(r'^users/$', views.SearchUsers.as_view(), name=views.SearchUsers.view_name), - re_path(r'^institutions/$', views.SearchInstitutions.as_view(), name=views.SearchInstitutions.view_name), - re_path(r'^collections/$', views.SearchCollections.as_view(), name=views.SearchCollections.view_name), - - # not currently supported by v1, but should be supported by v2 - # re_path(r'^nodes/$', views.SearchProjects.as_view(), name=views.SearchProjects.view_name), -] diff --git a/api/search/views.py b/api/search/views.py deleted file mode 100644 index b8bf9fba3af..00000000000 --- a/api/search/views.py +++ /dev/null @@ -1,682 +0,0 @@ -from rest_framework import generics -from rest_framework.exceptions import ValidationError -from rest_framework.response import Response - -from api.base import permissions as base_permissions -from api.base.views import JSONAPIBaseView -from api.base.pagination import SearchPagination -from api.base.parsers import SearchParser -from api.base.settings import REST_FRAMEWORK, MAX_PAGE_SIZE -from api.files.serializers import FileSerializer -from api.nodes.serializers import NodeSerializer -from api.registrations.serializers import RegistrationSerializer -from api.search.permissions import IsAuthenticatedOrReadOnlyForSearch -from api.search.serializers import SearchSerializer -from api.users.serializers import UserSerializer -from api.institutions.serializers import InstitutionSerializer -from api.collections.serializers import CollectionSubmissionSerializer - -from framework.auth.oauth_scopes import CoreScopes -from osf.models import Institution, BaseFileNode, AbstractNode, OSFUser, CollectionSubmission - -from website.search import search -from website.search.exceptions import MalformedQueryError -from website.search.util import build_query -from api.base.filters import ElasticOSFOrderingFilter - - -class BaseSearchView(JSONAPIBaseView, generics.ListCreateAPIView): - - required_read_scopes = [CoreScopes.SEARCH] - required_write_scopes = [CoreScopes.NULL] - - permission_classes = ( - IsAuthenticatedOrReadOnlyForSearch, - base_permissions.TokenHasScope, - ) - - pagination_class = SearchPagination - filter_backends = [ElasticOSFOrderingFilter] - - @property - def search_fields(self): - # Should be overridden in subclasses to provide a list of keys found - # in the relevant elastic doc. - raise NotImplementedError - - def __init__(self): - super().__init__() - self.doc_type = getattr(self, 'doc_type', None) - - def get_parsers(self): - if self.request.method == 'POST': - return (SearchParser(),) - return super().get_parsers() - - def get_queryset(self, query=None): - page = int(self.request.query_params.get('page', '1')) - page_size = min(int(self.request.query_params.get('page[size]', REST_FRAMEWORK['PAGE_SIZE'])), MAX_PAGE_SIZE) - start = (page - 1) * page_size - if query: - # Parser has built query, but needs paging info - query['from'] = start - query['size'] = page_size - else: - query = build_query(self.request.query_params.get('q', '*'), start=start, size=page_size) - try: - results = search.search(query, doc_type=self.doc_type, raw=True) - except MalformedQueryError as e: - raise ValidationError(e) - return results - - -class Search(BaseSearchView): - """ - *Read-Only* - - Objects (including projects, components, registrations, users, files, and institutions) that have been found by the given - Elasticsearch query. Each object is serialized with the appropriate serializer for its type (files are serialized as - files, users are serialized as users, etc.) and returned collectively. - - ## Search Fields - - # either projects, components, registrations, users, files, or institutions - related - href # the canonical api endpoint to search within a certain object type, e.g `/v2/search/users/` - meta - total # the number of results found that are of the enclosing object type - - ## Links - - See the [JSON-API spec regarding pagination](http://jsonapi.org/format/1.0/#fetching-pagination). - - ## Query Params - - + `q=` -- Query to search projects, components, registrations, users, and files for. - - + `page=` -- page number of results to view, default 1 - - # This Request/Response - - """ - - serializer_class = SearchSerializer - - view_category = 'search' - view_name = 'search-search' - - -class SearchComponents(BaseSearchView): - """ - *Read-Only* - - Components that have been found by the given Elasticsearch query. - - - - On the front end, nodes are considered 'projects' or 'components'. The difference between a project and a component - is that a project is the top-level node, and components are children of the project. There is also a [category - field](/v2/#osf-node-categories) that includes 'project' as an option. The categorization essentially determines - which icon is displayed by the node in the front-end UI and helps with search organization. Top-level nodes may have - a category other than project, and children nodes may have a category of project. - - ##Node Attributes - - - - OSF Node entities have the "nodes" `type`. - - name type description - ================================================================================= - title string title of project or component - description string description of the node - category string node category, must be one of the allowed values - date_created iso8601 timestamp timestamp that the node was created - date_modified iso8601 timestamp timestamp when the node was last updated - tags array of strings list of tags that describe the node - current_user_can_comment boolean Whether the current user is allowed to post comments - current_user_permissions array of strings list of strings representing the permissions for the current user on this node - registration boolean is this a registration? (always false - may be deprecated in future versions) - fork boolean is this node a fork of another node? - public boolean has this node been made publicly-visible? - preprint boolean is this a preprint? - collection boolean is this a collection? (always false - may be deprecated in future versions) - node_license object details of the license applied to the node - year string date range of the license - copyright_holders array of strings holders of the applied license - - ##Relationships - - ###Children - - List of nodes that are children of this node. New child nodes may be added through this endpoint. - - ###Comments - - List of comments on this node. New comments can be left on the node through this endpoint. - - ###Contributors - - List of users who are contributors to this node. Contributors may have "read", "write", or "admin" permissions. - A node must always have at least one "admin" contributor. Contributors may be added via this endpoint. - - ###Draft Registrations - - List of draft registrations of the current node. - - ###Files - - List of top-level folders (actually cloud-storage providers) associated with this node. This is the starting point - for accessing the actual files stored within this node. - - ###Forked From - - If this node was forked from another node, the canonical endpoint of the node that was forked from will be - available in the `/forked_from/links/related/href` key. Otherwise, it will be null. - - ###Logs - - List of read-only log actions pertaining to the node. - - ###Node Links - - List of links (pointers) to other nodes on the OSF. Node links can be added through this endpoint. - - ###Parent - - If this node is a child node of another node, the parent's canonical endpoint will be available in the - `/parent/links/related/href` key. Otherwise, it will be null. - - ###Registrations - - List of registrations of the current node. - - ###Root - - Returns the top-level node associated with the current node. If the current node is the top-level node, the root is - the current node. - - ##Links - - self: the canonical api endpoint of this node - html: this node's page on the OSF website - - See the [JSON-API spec regarding pagination](http://jsonapi.org/format/1.0/#fetching-pagination). - - ##Query Params - - + `q=` -- Query to search components for, searches across a component's title, description, tags, and contributor names. - - + `page=` -- page number of results to view, default 1 - - #This Request/Response - - """ - - model_class = AbstractNode - serializer_class = NodeSerializer - - doc_type = 'component' - view_category = 'search' - view_name = 'search-component' - - -class SearchFiles(BaseSearchView): - """ - *Read-Only* - - Files that have been found by the given Elasticsearch query. - - - - ####File Entity - - name type description - ========================================================================= - guid string OSF GUID for this file (if one has been assigned) - name string name of the file - path string unique identifier for this file entity for this - project and storage provider. may not end with '/' - materialized string the full path of the file relative to the storage - root. may not end with '/' - kind string "file" - etag string etag - http caching identifier w/o wrapping quotes - modified timestamp last modified timestamp - format depends on provider - contentType string MIME-type when available - provider string id of provider e.g. "osfstorage", "s3", "googledrive". - equivalent to addon_short_name on the OSF - size integer size of file in bytes - tags array of strings list of tags that describes the file (osfstorage only) - extra object may contain additional data beyond what's described here, - depending on the provider - version integer version number of file. will be 1 on initial upload - downloads integer count of the number times the file has been downloaded - hashes object - md5 string md5 hash of file - sha256 string SHA-256 hash of file - - ##Attributes - - For an OSF File entity, the `type` is "files" regardless of whether the entity is actually a file or folder, because - it belongs to the `files` collection of the API. They can be distinguished by the `kind` attribute. Files and - folders use the same representation, but some attributes may be null for one kind but not the other. `size` will be - null for folders. See the [list of storage provider keys](/v2/#storage-providers). - - name type description - ================================================================================================================ - name string name of the file or folder; used for display - kind string "file" or "folder" - path string same as for corresponding WaterButler entity - materialized_path string the unix-style path to the file relative to the provider root - size integer size of file in bytes, null for folders - provider string storage provider for this file. "osfstorage" if stored on the - OSF. other examples include "s3" for Amazon S3, "googledrive" - for Google Drive, "box" for Box.com. - current_user_can_comment boolean Whether the current user is allowed to post comments - - last_touched iso8601 timestamp last time the metadata for the file was retrieved. only - applies to non-OSF storage providers. - date_modified iso8601 timestamp timestamp of when this file was last updated* - date_created iso8601 timestamp timestamp of when this file was created* - extra object may contain additional data beyond what's described here, - depending on the provider - hashes object - md5 string md5 hash of file, null for folders - sha256 string SHA-256 hash of file, null for folders - - * A note on timestamps: for files stored in osfstorage, `date_created` refers to the time that the file was - first uploaded to osfstorage, and `date_modified` is the time that the file was last updated while in osfstorage. - Other providers may or may not provide this information, but if they do it will correspond to the provider's - semantics for created/modified times. These timestamps may also be stale; metadata retrieved via the File Detail - endpoint is cached. The `last_touched` field describes the last time the metadata was retrieved from the external - provider. To force a metadata update, access the parent folder via its Node Files List endpoint. - - - - ##Relationships - - ###Node - - The `node` endpoint describes the project or registration that this file belongs to. - - ###Files (*folders*) - - The `files` endpoint lists all of the subfiles and folders of the current folder. Will be null for files. - - ###Versions (*files*) - - The `versions` endpoint provides version history for files. Will be null for folders. - - ##Links - - info: the canonical api endpoint for the folder's contents or file's most recent version - new_folder: url to target when creating new subfolders (null for files) - move: url to target for move, copy, and rename actions - upload: url to target for uploading new files and updating existing files - download: url to request a download of the latest version of the file (null for folders) - delete: url to target for deleting files and folders - - ## Query Params - - + `q=` -- Query to search files for, searches across a file's name. - - + `page=` -- page number of results to view, default 1 - - #This Request/Response - - """ - - model_class = BaseFileNode - serializer_class = FileSerializer - - doc_type = 'file' - view_category = 'search' - view_name = 'search-file' - - -class SearchProjects(BaseSearchView): - """ - *Read-Only* - - Projects that have been found by the given Elasticsearch query. - - - - On the front end, nodes are considered 'projects' or 'components'. The difference between a project and a component - is that a project is the top-level node, and components are children of the project. There is also a [category - field](/v2/#osf-node-categories) that includes 'project' as an option. The categorization essentially determines - which icon is displayed by the node in the front-end UI and helps with search organization. Top-level nodes may have - a category other than project, and children nodes may have a category of project. - - ##Node Attributes - - - - OSF Node entities have the "nodes" `type`. - - name type description - ================================================================================= - title string title of project or component - description string description of the node - category string node category, must be one of the allowed values - date_created iso8601 timestamp timestamp that the node was created - date_modified iso8601 timestamp timestamp when the node was last updated - tags array of strings list of tags that describe the node - current_user_can_comment boolean Whether the current user is allowed to post comments - current_user_permissions array of strings list of strings representing the permissions for the current user on this node - registration boolean is this a registration? (always false - may be deprecated in future versions) - fork boolean is this node a fork of another node? - public boolean has this node been made publicly-visible? - preprint boolean is this a preprint? - collection boolean is this a collection? (always false - may be deprecated in future versions) - node_license object details of the license applied to the node - year string date range of the license - copyright_holders array of strings holders of the applied license - - ##Relationships - - ###Children - - List of nodes that are children of this node. New child nodes may be added through this endpoint. - - ###Comments - - List of comments on this node. New comments can be left on the node through this endpoint. - - ###Contributors - - List of users who are contributors to this node. Contributors may have "read", "write", or "admin" permissions. - A node must always have at least one "admin" contributor. Contributors may be added via this endpoint. - - ###Draft Registrations - - List of draft registrations of the current node. - - ###Files - - List of top-level folders (actually cloud-storage providers) associated with this node. This is the starting point - for accessing the actual files stored within this node. - - ###Forked From - - If this node was forked from another node, the canonical endpoint of the node that was forked from will be - available in the `/forked_from/links/related/href` key. Otherwise, it will be null. - - ###Logs - - List of read-only log actions pertaining to the node. - - ###Node Links - - List of links (pointers) to other nodes on the OSF. Node links can be added through this endpoint. - - ###Parent - - If this node is a child node of another node, the parent's canonical endpoint will be available in the - `/parent/links/related/href` key. Otherwise, it will be null. - - ###Registrations - - List of registrations of the current node. - - ###Root - - Returns the top-level node associated with the current node. If the current node is the top-level node, the root is - the current node. - - ##Links - - self: the canonical api endpoint of this node - html: this node's page on the OSF website - - See the [JSON-API spec regarding pagination](http://jsonapi.org/format/1.0/#fetching-pagination). - - ##Query Params - - + `q=` -- Query to search projects for, searches across a project's title, description, tags, and contributor names. - - + `page=` -- page number of results to view, default 1 - - - #This Request/Response - - """ - - model_class = AbstractNode - serializer_class = NodeSerializer - - doc_type = 'project' - view_category = 'search' - view_name = 'search-project' - - -class SearchRegistrations(BaseSearchView): - """ - *Read-Only* - - Registrations that have been found by the given Elasticsearch query. - - - - Node Registrations. - - Registrations are read-only snapshots of a project. This view is a list of all current registrations for which a user - has access. A withdrawn registration will display a limited subset of information, namely, title, description, - created, registration, withdrawn, date_registered, withdrawal_justification, and registration supplement. All - other fields will be displayed as null. Additionally, the only relationships permitted to be accessed for a withdrawn - registration are the contributors - other relationships will return a 403. - - Each resource contains the full representation of the registration, meaning additional requests to an individual - registrations's detail view are not necessary. Unregistered nodes cannot be accessed through this endpoint. - - - ##Registration Attributes - - Registrations have the "registrations" `type`. - - name type description - ======================================================================================================= - title string title of the registered project or component - description string description of the registered node - category string bode category, must be one of the allowed values - date_created iso8601 timestamp timestamp that the node was created - date_modified iso8601 timestamp timestamp when the node was last updated - tags array of strings list of tags that describe the registered node - current_user_can_comment boolean Whether the current user is allowed to post comments - current_user_permissions array of strings list of strings representing the permissions for the current user on this node - fork boolean is this project a fork? - registration boolean has this project been registered? (always true - may be deprecated in future versions) - collection boolean is this registered node a collection? (always false - may be deprecated in future versions) - node_license object details of the license applied to the node - year string date range of the license - copyright_holders array of strings holders of the applied license - public boolean has this registration been made publicly-visible? - withdrawn boolean has this registration been withdrawn? - date_registered iso8601 timestamp timestamp that the registration was created - embargo_end_date iso8601 timestamp when the embargo on this registration will be lifted (if applicable) - withdrawal_justification string reasons for withdrawing the registration - 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 - - - - ##Relationships - - ###Registered from - - The registration is branched from this node. - - ###Registered by - - The registration was initiated by this user. - - ###Other Relationships - - See documentation on registered_from detail view. A registration has many of the same properties as a node. - - ##Links - - See the [JSON-API spec regarding pagination](http://jsonapi.org/format/1.0/#fetching-pagination). - - ## Query Params - - + `q=` -- Query to search registrations for, searches across a registration's title, description, tags, and contributor names. - - + `page=` -- page number of results to view, default 1 - - #This Request/Response - - """ - - model_class = AbstractNode - serializer_class = RegistrationSerializer - - doc_type = 'registration' - view_category = 'search' - view_name = 'search-registration' - - -class SearchUsers(BaseSearchView): - """ - *Read-Only* - - Users that have been found by the given Elasticsearch query. - - - - The User Detail endpoint retrieves information about the user whose id is the final part of the path. If `me` - is given as the id, the record of the currently logged-in user will be returned. The returned information includes - the user's bibliographic information and the date that the user registered. - - Note that if an anonymous view_only key is being used, user information will not be serialized, and the id will be - an empty string. Relationships to a user object will not show in this case, either. - - - - ##Attributes - - OSF User entities have the "users" `type`. - - name type description - ======================================================================================== - full_name string full name of the user; used for display - given_name string given name of the user; for bibliographic citations - middle_names string middle name of user; for bibliographic citations - family_name string family name of user; for bibliographic citations - suffix string suffix of user's name for bibliographic citations - date_registered iso8601 timestamp timestamp when the user's account was created - - - - ##Relationships - - ###Nodes - - A list of all nodes the user has contributed to. If the user id in the path is the same as the logged-in user, all - nodes will be visible. Otherwise, you will only be able to see the other user's publicly-visible nodes. - - ##Links - - self: the canonical api endpoint of this user - html: this user's page on the OSF website - profile_image_url: a url to the user's profile image - - ## Query Params - - + `q=` -- Query to search users for, searches across a users's given name, middle names, family name, - first listed job, and first listed school. - - + `page=` -- page number of results to view, default 1 - - # This Request/Response - - """ - - model_class = OSFUser - serializer_class = UserSerializer - - doc_type = 'user' - view_category = 'search' - view_name = 'search-user' - - -class SearchInstitutions(BaseSearchView): - """ - *Read-Only* - - Institutions that have been found by the given Elasticsearch query. - - - - ##Attributes - - OSF Institutions have the "institutions" `type`. - - name type description - ========================================================================= - name string title of the institution - id string unique identifier in the OSF - logo_path string a path to the institution's static logo - - ##Relationships - - ###Nodes - List of nodes that have this institution as its primary institution. - - ###Users - List of users that are affiliated with this institution. - - ##Links - - self: the canonical api endpoint of this institution - html: this institution's page on the OSF website - - # This Request/Response - - """ - - model_class = Institution - serializer_class = InstitutionSerializer - - doc_type = 'institution' - view_category = 'search' - view_name = 'search-institution' - - -class SearchCollections(BaseSearchView): - """ - """ - - model_class = CollectionSubmission - serializer_class = CollectionSubmissionSerializer - - doc_type = 'collectionSubmission' - view_category = 'search' - view_name = 'search-collected-metadata' - required_write_scopes = [CoreScopes.ADVANCED_SEARCH] - - @property - def search_fields(self): - return [ - 'abstract', - 'collectedType', - 'contributors.fullname', - 'status', - 'subjects', - 'provider', - 'title', - 'tags', - ] - - def create(self, request, *args, **kwargs): - # Override POST methods to behave like list, with header query parsing - queryset = self.filter_queryset(self.get_queryset(request.data)) - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - return self.get_paginated_response(serializer.data) - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) diff --git a/api_tests/base/test_views.py b/api_tests/base/test_views.py index 2d6b029b6ba..e5b0d23edeb 100644 --- a/api_tests/base/test_views.py +++ b/api_tests/base/test_views.py @@ -9,7 +9,6 @@ from framework.auth.oauth_scopes import CoreScopes from api.base.settings.defaults import API_BASE -from api.search.permissions import IsAuthenticatedOrReadOnlyForSearch from api.crossref.views import ParseCrossRefConfirmation from api.metrics.views import ( RawMetricsView, @@ -102,7 +101,7 @@ def test_does_not_exist_formatting(self): def test_view_classes_have_minimal_set_of_permissions_classes(self): base_permissions = [ TokenHasScope, - (IsAuthenticated, IsAuthenticatedOrReadOnly, IsAuthenticatedOrReadOnlyForSearch) + (IsAuthenticated, IsAuthenticatedOrReadOnly) ] for view in VIEW_CLASSES: if view in self.EXCLUDED_VIEWS: diff --git a/api_tests/search/__init__.py b/api_tests/search/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api_tests/search/serializers/__init__.py b/api_tests/search/serializers/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api_tests/search/serializers/test_serializers.py b/api_tests/search/serializers/test_serializers.py deleted file mode 100644 index 92f267f77f9..00000000000 --- a/api_tests/search/serializers/test_serializers.py +++ /dev/null @@ -1,50 +0,0 @@ -import pytest - -from api.search.serializers import SearchSerializer -from api_tests import utils -from osf.models import RegistrationSchema -from osf_tests.factories import ( - AuthUserFactory, - NodeFactory, - ProjectFactory, -) -from tests.utils import make_drf_request_with_version, mock_archive - -SCHEMA_VERSION = 2 - - -@pytest.mark.django_db -class TestSearchSerializer: - - @pytest.mark.usefixtures('mock_gravy_valet_get_verified_links') - def test_search_serializer_mixed_model(self): - - user = AuthUserFactory() - project = ProjectFactory(creator=user, is_public=True) - component = NodeFactory(parent=project, creator=user, is_public=True) - file_component = utils.create_test_file(component, user) - context = {'request': make_drf_request_with_version(version='2.0')} - schema = RegistrationSchema.objects.filter( - name='Replication Recipe (Brandt et al., 2013): Post-Completion', - schema_version=SCHEMA_VERSION).first() - - # test_search_serializer_mixed_model_project - result = SearchSerializer(project, context=context).data - assert result['data']['type'] == 'nodes' - - # test_search_serializer_mixed_model_component - result = SearchSerializer(component, context=context).data - assert result['data']['type'] == 'nodes' - - # test_search_serializer_mixed_model_registration - with mock_archive(project, autocomplete=True, autoapprove=True, schema=schema) as registration: - result = SearchSerializer(registration, context=context).data - assert result['data']['type'] == 'registrations' - - # test_search_serializer_mixed_model_file - result = SearchSerializer(file_component, context=context).data - assert result['data']['type'] == 'files' - - # test_search_serializer_mixed_model_user - result = SearchSerializer(user, context=context).data - assert result['data']['type'] == 'users' diff --git a/api_tests/search/views/__init__.py b/api_tests/search/views/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api_tests/search/views/test_views.py b/api_tests/search/views/test_views.py deleted file mode 100644 index c535828d955..00000000000 --- a/api_tests/search/views/test_views.py +++ /dev/null @@ -1,1056 +0,0 @@ -import pytest -import uuid -from unittest import mock - -from api.base.settings.defaults import API_BASE -from api_tests import utils -from framework.auth.cas import CasResponse -from framework.auth.core import Auth -from osf.models import RegistrationSchema -from osf_tests.factories import ( - AuthUserFactory, - NodeFactory, - ProjectFactory, - RegistrationFactory, - InstitutionFactory, - CollectionFactory, - CollectionProviderFactory, - RegistrationProviderFactory, -) -from osf_tests.utils import mock_archive -from tests.utils import capture_notifications -from website import settings -from website.search import elastic_search -from website.search import search - -SCHEMA_VERSION = 2 - - -@pytest.mark.django_db -@pytest.mark.enable_search -@pytest.mark.enable_enqueue_task -class ApiSearchTestCase: - - @pytest.fixture(autouse=True) - def index(self): - settings.ELASTIC_INDEX = uuid.uuid4().hex - elastic_search.INDEX = settings.ELASTIC_INDEX - - search.create_index(elastic_search.INDEX) - yield - search.delete_index(elastic_search.INDEX) - - @pytest.fixture() - def user(self): - return AuthUserFactory() - - @pytest.fixture() - def institution(self): - return InstitutionFactory(name='Social Experiment') - - @pytest.fixture() - def collection_public(self, user): - return CollectionFactory(creator=user, provider=CollectionProviderFactory(), is_public=True, - status_choices=['', 'asdf', 'lkjh'], collected_type_choices=['', 'asdf', 'lkjh'], - issue_choices=['', '0', '1', '2'], volume_choices=['', '0', '1', '2'], - disease_choices=['illness'], data_type_choices=['realness'], - program_area_choices=['', 'asdf', 'lkjh'], grade_levels_choices=['super', 'cool']) - - @pytest.fixture() - def registration_collection(self, user): - return CollectionFactory(creator=user, provider=RegistrationProviderFactory(), is_public=True, - status_choices=['', 'asdf', 'lkjh'], collected_type_choices=['', 'asdf', 'lkjh']) - - @pytest.fixture() - def user_one(self): - user_one = AuthUserFactory(fullname='Kanye Omari West') - user_one.schools = [{ - 'degree': 'English', - 'institution': 'Chicago State University' - }] - user_one.jobs = [{ - 'title': 'Producer', - 'institution': 'GOOD Music, Inc.' - }] - user_one.save() - return user_one - - @pytest.fixture() - def user_two(self, institution): - user_two = AuthUserFactory(fullname='Chance The Rapper') - user_two.add_or_update_affiliated_institution(institution) - user_two.save() - return user_two - - @pytest.fixture() - def project(self, user_one): - project = ProjectFactory( - title='Graduation', - creator=user_one, - is_public=True) - project.update_search() - return project - - @pytest.fixture() - def project_public(self, user_one): - project_public = ProjectFactory( - title='The Life of Pablo', - creator=user_one, - is_public=True) - project_public.set_description( - 'Name one genius who ain\'t crazy', - auth=Auth(user_one), - save=True) - project_public.add_tag('Yeezus', auth=Auth(user_one), save=True) - return project_public - - @pytest.fixture() - def project_private(self, user_two): - return ProjectFactory(title='Coloring Book', creator=user_two) - - @pytest.fixture() - def component(self, user_one, project_public): - return NodeFactory( - parent=project_public, - title='Highlights', - description='', - creator=user_one, - is_public=True) - - @pytest.fixture() - def component_public(self, user_two, project_public): - component_public = NodeFactory( - parent=project_public, - title='Ultralight Beam', - creator=user_two, - is_public=True) - component_public.set_description( - 'This is my part, nobody else speak', - auth=Auth(user_two), - save=True) - component_public.add_tag('trumpets', auth=Auth(user_two), save=True) - return component_public - - @pytest.fixture() - def component_private(self, user_one, project_public): - return NodeFactory( - parent=project_public, - description='', - title='Wavves', - creator=user_one) - - @pytest.fixture() - def file_component(self, component, user_one): - return utils.create_test_file( - component, user_one, filename='Highlights.mp3') - - @pytest.fixture() - def file_public(self, component_public, user_one): - return utils.create_test_file( - component_public, - user_one, - filename='UltralightBeam.mp3') - - @pytest.fixture() - def file_private(self, component_private, user_one): - return utils.create_test_file( - component_private, user_one, filename='Wavves.mp3') - - -class TestSearch(ApiSearchTestCase): - - @pytest.fixture() - def url_search(self): - return f'/{API_BASE}search/' - - def test_search_results( - self, app, url_search, user, user_one, user_two, - institution, component, component_private, - component_public, file_component, file_private, - file_public, project, project_public, project_private): - - # test_search_no_auth - res = app.get(url_search) - assert res.status_code == 200 - - search_fields = res.json['search_fields'] - users_found = search_fields['users']['related']['meta']['total'] - files_found = search_fields['files']['related']['meta']['total'] - projects_found = search_fields['projects']['related']['meta']['total'] - components_found = search_fields['components']['related']['meta']['total'] - registrations_found = search_fields['registrations']['related']['meta']['total'] - - assert users_found == 3 - assert files_found == 2 - assert projects_found == 2 - assert components_found == 2 - assert registrations_found == 0 - - # test_search_auth - res = app.get(url_search, auth=user.auth) - assert res.status_code == 200 - - search_fields = res.json['search_fields'] - users_found = search_fields['users']['related']['meta']['total'] - files_found = search_fields['files']['related']['meta']['total'] - projects_found = search_fields['projects']['related']['meta']['total'] - components_found = search_fields['components']['related']['meta']['total'] - registrations_found = search_fields['registrations']['related']['meta']['total'] - - assert users_found == 3 - assert files_found == 2 - assert projects_found == 2 - assert components_found == 2 - assert registrations_found == 0 - - # test_search_fields_links - res = app.get(url_search) - assert res.status_code == 200 - - search_fields = res.json['search_fields'] - users_link = search_fields['users']['related']['href'] - files_link = search_fields['files']['related']['href'] - projects_link = search_fields['projects']['related']['href'] - components_link = search_fields['components']['related']['href'] - registrations_link = search_fields['registrations']['related']['href'] - - assert f'/{API_BASE}search/users/?q=%2A' in users_link - assert f'/{API_BASE}search/files/?q=%2A' in files_link - assert f'/{API_BASE}search/projects/?q=%2A' in projects_link - assert '/{}search/components/?q=%2A'.format( - API_BASE) in components_link - assert '/{}search/registrations/?q=%2A'.format( - API_BASE) in registrations_link - - # test_search_fields_links_with_query - url = f'{url_search}?q=science' - res = app.get(url) - assert res.status_code == 200 - - search_fields = res.json['search_fields'] - users_link = search_fields['users']['related']['href'] - files_link = search_fields['files']['related']['href'] - projects_link = search_fields['projects']['related']['href'] - components_link = search_fields['components']['related']['href'] - registrations_link = search_fields['registrations']['related']['href'] - - assert f'/{API_BASE}search/users/?q=science' in users_link - assert f'/{API_BASE}search/files/?q=science' in files_link - assert '/{}search/projects/?q=science'.format( - API_BASE) in projects_link - assert '/{}search/components/?q=science'.format( - API_BASE) in components_link - assert '/{}search/registrations/?q=science'.format( - API_BASE) in registrations_link - - -class TestSearchComponents(ApiSearchTestCase): - - @pytest.fixture() - def url_component_search(self): - return f'/{API_BASE}search/components/' - - def test_search_components( - self, app, url_component_search, user, user_one, user_two, - component, component_public, component_private): - - # test_search_public_component_no_auth - res = app.get(url_component_search) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert component_public.title in res - assert component.title in res - - # test_search_public_component_auth - res = app.get(url_component_search, auth=user) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert component_public.title in res - assert component.title in res - - # test_search_public_component_contributor - res = app.get(url_component_search, auth=user_two) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert component_public.title in res - assert component.title in res - - # test_search_private_component_no_auth - res = app.get(url_component_search) - assert res.status_code == 200 - assert component_private.title not in res - - # test_search_private_component_auth - res = app.get(url_component_search, auth=user) - assert res.status_code == 200 - assert component_private.title not in res - - # test_search_private_component_contributor - res = app.get(url_component_search, auth=user_two) - assert res.status_code == 200 - assert component_private.title not in res - - # test_search_component_by_title - url = '{}?q={}'.format(url_component_search, 'beam') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert component_public.title == res.json['data'][0]['attributes']['title'] - - # test_search_component_by_description - url = '{}?q={}'.format(url_component_search, 'speak') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert component_public.title == res.json['data'][0]['attributes']['title'] - - # test_search_component_by_tags - url = '{}?q={}'.format(url_component_search, 'trumpets') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert component_public.title == res.json['data'][0]['attributes']['title'] - - # test_search_component_by_contributor - url = '{}?q={}'.format(url_component_search, 'Chance') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert component_public.title == res.json['data'][0]['attributes']['title'] - - # test_search_component_no_results - url = '{}?q={}'.format(url_component_search, 'Ocean') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 0 - assert total == 0 - - # test_search_component_bad_query - url = '{}?q={}'.format( - url_component_search, - 'www.spam.com/help/twitter/') - res = app.get(url, expect_errors=True) - assert res.status_code == 400 - - -class TestSearchFiles(ApiSearchTestCase): - - @pytest.fixture() - def url_file_search(self): - return f'/{API_BASE}search/files/' - - def test_search_files( - self, app, url_file_search, user, user_one, - file_public, file_component, file_private): - - # test_search_public_file_no_auth - res = app.get(url_file_search) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert file_public.name in res - assert file_component.name in res - - # test_search_public_file_auth - res = app.get(url_file_search, auth=user) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert file_public.name in res - assert file_component.name in res - - # test_search_public_file_contributor - res = app.get(url_file_search, auth=user_one) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert file_public.name in res - assert file_component.name in res - - # test_search_private_file_no_auth - res = app.get(url_file_search) - assert res.status_code == 200 - assert file_private.name not in res - - # test_search_private_file_auth - res = app.get(url_file_search, auth=user) - assert res.status_code == 200 - assert file_private.name not in res - - # test_search_private_file_contributor - res = app.get(url_file_search, auth=user_one) - assert res.status_code == 200 - assert file_private.name not in res - - # test_search_file_by_name - url = '{}?q={}'.format(url_file_search, 'highlights') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert file_component.name == res.json['data'][0]['attributes']['name'] - - -class TestSearchProjects(ApiSearchTestCase): - - @pytest.fixture() - def url_project_search(self): - return f'/{API_BASE}search/projects/' - - def test_search_projects( - self, app, url_project_search, user, user_one, - user_two, project, project_public, project_private): - - # test_search_public_project_no_auth - res = app.get(url_project_search) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert project_public.title in res - assert project.title in res - - # test_search_public_project_auth - res = app.get(url_project_search, auth=user) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert project_public.title in res - assert project.title in res - - # test_search_public_project_contributor - res = app.get(url_project_search, auth=user_one) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert project_public.title in res - assert project.title in res - - # test_search_private_project_no_auth - res = app.get(url_project_search) - assert res.status_code == 200 - assert project_private.title not in res - - # test_search_private_project_auth - res = app.get(url_project_search, auth=user) - assert res.status_code == 200 - assert project_private.title not in res - - # test_search_private_project_contributor - res = app.get(url_project_search, auth=user_two) - assert res.status_code == 200 - assert project_private.title not in res - - # test_search_project_by_title - url = '{}?q={}'.format(url_project_search, 'pablo') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert project_public.title == res.json['data'][0]['attributes']['title'] - - # test_search_project_by_description - url = '{}?q={}'.format(url_project_search, 'genius') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert project_public.title == res.json['data'][0]['attributes']['title'] - - # test_search_project_by_tags - url = '{}?q={}'.format(url_project_search, 'Yeezus') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert project_public.title == res.json['data'][0]['attributes']['title'] - - # test_search_project_by_contributor - url = '{}?q={}'.format(url_project_search, 'kanye') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert project_public.title in res - assert project.title in res - - # test_search_project_no_results - url = '{}?q={}'.format(url_project_search, 'chicago') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 0 - assert total == 0 - - # test_search_project_bad_query - url = '{}?q={}'.format( - url_project_search, - 'www.spam.com/help/facebook/') - res = app.get(url, expect_errors=True) - assert res.status_code == 400 - - -@pytest.mark.django_db -class TestSearchRegistrations(ApiSearchTestCase): - - @pytest.fixture() - def url_registration_search(self): - return f'/{API_BASE}search/registrations/' - - @pytest.fixture() - def schema(self): - schema = RegistrationSchema.objects.filter( - name='Replication Recipe (Brandt et al., 2013): Post-Completion', - schema_version=SCHEMA_VERSION).first() - return schema - - @pytest.fixture() - def registration(self, project, schema): - with mock_archive(project, autocomplete=True, autoapprove=True, schema=schema) as registration: - return registration - - @pytest.fixture() - def registration_public(self, project_public, schema): - with mock_archive(project_public, autocomplete=True, autoapprove=True, schema=schema) as registration_public: - return registration_public - - @pytest.fixture() - def registration_private(self, project_private, schema): - with mock_archive(project_private, autocomplete=True, autoapprove=True, schema=schema) as registration_private: - registration_private.is_public = False - registration_private.save() - # TODO: This shouldn't be necessary, but tests fail if we don't do - # this. Investigate further. - registration_private.update_search() - return registration_private - - @pytest.mark.usefixtures('mock_gravy_valet_get_verified_links') - def test_search_registrations( - self, app, url_registration_search, user, user_one, user_two, - registration, registration_public, registration_private): - - # test_search_public_registration_no_auth - res = app.get(url_registration_search) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert registration_public.title in res - assert registration.title in res - - # test_search_public_registration_auth - res = app.get(url_registration_search, auth=user) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert registration_public.title in res - assert registration.title in res - - # test_search_public_registration_contributor - res = app.get(url_registration_search, auth=user_one) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert registration_public.title in res - assert registration.title in res - - # test_search_private_registration_no_auth - res = app.get(url_registration_search) - assert res.status_code == 200 - assert registration_private.title not in res - - # test_search_private_registration_auth - res = app.get(url_registration_search, auth=user) - assert res.status_code == 200 - assert registration_private.title not in res - - # test_search_private_registration_contributor - res = app.get(url_registration_search, auth=user_two) - assert res.status_code == 200 - assert registration_private.title not in res - - # test_search_registration_by_title - url = '{}?q={}'.format(url_registration_search, 'graduation') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert registration.title == res.json['data'][0]['attributes']['title'] - - # test_search_registration_by_description - url = '{}?q={}'.format(url_registration_search, 'crazy') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert registration_public.title == res.json['data'][0]['attributes']['title'] - - # test_search_registration_by_tags - url = '{}?q={}'.format(url_registration_search, 'yeezus') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert registration_public.title == res.json['data'][0]['attributes']['title'] - - # test_search_registration_by_contributor - url = '{}?q={}'.format(url_registration_search, 'west') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 2 - assert total == 2 - assert registration_public.title in res - assert registration.title in res - - # test_search_registration_no_results - url = '{}?q={}'.format(url_registration_search, '79th') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 0 - assert total == 0 - - # test_search_registration_bad_query - url = '{}?q={}'.format( - url_registration_search, - 'www.spam.com/help/snapchat/') - res = app.get(url, expect_errors=True) - assert res.status_code == 400 - - -class TestSearchUsers(ApiSearchTestCase): - - @pytest.fixture() - def url_user_search(self): - return f'/{API_BASE}search/users/' - - def test_search_user(self, app, url_user_search, user, user_one, user_two): - - # test_search_users_no_auth - res = app.get(url_user_search) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 3 - assert total == 3 - assert user.fullname in res - - # test_search_users_auth - res = app.get(url_user_search, auth=user) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 3 - assert total == 3 - assert user.fullname in res - - # test_search_users_by_given_name - url = '{}?q={}'.format(url_user_search, 'Kanye') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert user_one.given_name == res.json['data'][0]['attributes']['given_name'] - - # test_search_users_by_middle_name - url = '{}?q={}'.format(url_user_search, 'Omari') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert user_one.middle_names[0] == res.json['data'][0]['attributes']['middle_names'][0] - - # test_search_users_by_family_name - url = '{}?q={}'.format(url_user_search, 'West') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert user_one.family_name == res.json['data'][0]['attributes']['family_name'] - - # test_search_users_by_job - url = '{}?q={}'.format(url_user_search, 'producer') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert user_one.fullname == res.json['data'][0]['attributes']['full_name'] - - # test_search_users_by_school - url = '{}?q={}'.format(url_user_search, 'Chicago') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert user_one.fullname == res.json['data'][0]['attributes']['full_name'] - - -class TestSearchInstitutions(ApiSearchTestCase): - - @pytest.fixture() - def url_institution_search(self): - return f'/{API_BASE}search/institutions/' - - def test_search_institutions( - self, app, url_institution_search, user, institution): - - # test_search_institutions_no_auth - res = app.get(url_institution_search) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert institution.name in res - - # test_search_institutions_auth - res = app.get(url_institution_search, auth=user) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert institution.name in res - - # test_search_institutions_by_name - url = '{}?q={}'.format(url_institution_search, 'Social') - res = app.get(url) - assert res.status_code == 200 - num_results = len(res.json['data']) - total = res.json['links']['meta']['total'] - assert num_results == 1 - assert total == 1 - assert institution.name == res.json['data'][0]['attributes']['name'] - -class TestSearchCollections(ApiSearchTestCase): - - def get_ids(self, data): - return [s['id'] for s in data] - - def post_payload(self, *args, **kwargs): - return { - 'data': { - 'attributes': kwargs - }, - 'type': 'search' - } - - @pytest.fixture() - def url_collection_search(self): - return f'/{API_BASE}search/collections/' - - @pytest.fixture() - def node_one(self, user): - return NodeFactory(title='Ismael Lo: Tajabone', creator=user, is_public=True) - - @pytest.fixture() - def registration_one(self, node_one): - return RegistrationFactory(project=node_one, is_public=True) - - @pytest.fixture() - def node_two(self, user): - return NodeFactory(title='Sambolera', creator=user, is_public=True) - - @pytest.fixture() - def registration_two(self, node_two): - return RegistrationFactory(project=node_two, is_public=True) - - @pytest.fixture() - def node_private(self, user): - return NodeFactory(title='Classified', creator=user) - - @pytest.fixture() - def registration_private(self, node_private): - return RegistrationFactory(project=node_private, is_public=False) - - @pytest.fixture() - def node_with_abstract(self, user): - node_with_abstract = NodeFactory(title='Sambolera', creator=user, is_public=True) - node_with_abstract.set_description( - 'Sambolera by Khadja Nin', - auth=Auth(user), - save=True) - return node_with_abstract - - @pytest.fixture() - def reg_with_abstract(self, node_with_abstract): - return RegistrationFactory(project=node_with_abstract, is_public=True) - - def test_search_collections( - self, app, url_collection_search, user, node_one, node_two, collection_public, - node_with_abstract, node_private, registration_collection, registration_one, registration_two, - registration_private, reg_with_abstract): - - with capture_notifications(): - collection_public.collect_object(node_one, user) - collection_public.collect_object(node_two, user) - collection_public.collect_object(node_private, user) - - registration_collection.collect_object(registration_one, user) - registration_collection.collect_object(registration_two, user) - registration_collection.collect_object(registration_private, user) - - # test_search_collections_no_auth - res = app.get(url_collection_search) - assert res.status_code == 200 - total = res.json['links']['meta']['total'] - num_results = len(res.json['data']) - assert total == 4 - assert num_results == 4 - actual_ids = self.get_ids(res.json['data']) - assert registration_private._id not in actual_ids - assert node_private._id not in actual_ids - - # test_search_collections_auth - res = app.get(url_collection_search, auth=user) - assert res.status_code == 200 - total = res.json['links']['meta']['total'] - num_results = len(res.json['data']) - assert total == 4 - assert num_results == 4 - actual_ids = self.get_ids(res.json['data']) - assert registration_private._id not in actual_ids - assert node_private._id not in actual_ids - - # test_search_collections_by_submission_title - url = '{}?q={}'.format(url_collection_search, 'Ismael') - res = app.get(url) - assert res.status_code == 200 - total = res.json['links']['meta']['total'] - num_results = len(res.json['data']) - assert node_one.title == registration_one.title == res.json['data'][0]['embeds']['guid']['data']['attributes']['title'] - assert total == num_results == 2 - - # test_search_collections_by_submission_abstract - with capture_notifications(): - collection_public.collect_object(node_with_abstract, user) - registration_collection.collect_object(reg_with_abstract, user) - url = '{}?q={}'.format(url_collection_search, 'KHADJA') - res = app.get(url) - assert res.status_code == 200 - total = res.json['links']['meta']['total'] - assert node_with_abstract.description == reg_with_abstract.description == res.json['data'][0]['embeds']['guid']['data']['attributes']['description'] - assert total == 2 - - # test_search_collections_no_results: - url = '{}?q={}'.format(url_collection_search, 'Wale Watu') - res = app.get(url) - assert res.status_code == 200 - total = res.json['links']['meta']['total'] - assert total == 0 - - def test_POST_search_collections( - self, app, url_collection_search, user, node_one, node_two, collection_public, - node_with_abstract, node_private, registration_collection, registration_one, - registration_two, registration_private, reg_with_abstract): - with capture_notifications(): - collection_public.collect_object(node_one, user, status='asdf', issue='0', volume='1', program_area='asdf') - collection_public.collect_object(node_two, user, collected_type='asdf', status='lkjh') - collection_public.collect_object(node_with_abstract, user, status='asdf', grade_levels='super') - collection_public.collect_object(node_private, user, status='asdf', collected_type='asdf') - - registration_collection.collect_object(registration_one, user, status='asdf') - registration_collection.collect_object(registration_two, user, collected_type='asdf', status='lkjh') - registration_collection.collect_object(reg_with_abstract, user, status='asdf') - registration_collection.collect_object(registration_private, user, status='asdf', collected_type='asdf') - - # test_search_empty - payload = self.post_payload() - res = app.post_json_api(url_collection_search, payload) - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 6 - assert len(res.json['data']) == 6 - actual_ids = self.get_ids(res.json['data']) - assert registration_private._id not in actual_ids - assert node_private._id not in actual_ids - - # test_search_title_keyword - payload = self.post_payload(q='Ismael') - res = app.post_json_api(url_collection_search, payload) - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 2 - assert len(res.json['data']) == 2 - actual_ids = self.get_ids(res.json['data']) - assert registration_private._id not in actual_ids - assert node_private._id not in actual_ids - - # test_search_abstract_keyword - payload = self.post_payload(q='Khadja') - res = app.post_json_api(url_collection_search, payload) - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 2 - assert len(res.json['data']) == 2 - actual_ids = self.get_ids(res.json['data']) - assert node_with_abstract._id in actual_ids - assert reg_with_abstract._id in actual_ids - - # test_search_filter - payload = self.post_payload(status='asdf') - res = app.post_json_api(url_collection_search, payload) - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 4 - assert len(res.json['data']) == 4 - actual_ids = self.get_ids(res.json['data']) - assert registration_private._id not in actual_ids - assert node_private._id not in actual_ids - - payload = self.post_payload(status=['asdf', 'lkjh']) - res = app.post_json_api(url_collection_search, payload) - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 6 - assert len(res.json['data']) == 6 - actual_ids = self.get_ids(res.json['data']) - assert registration_private._id not in actual_ids - assert node_private._id not in actual_ids - - payload = self.post_payload(collectedType='asdf') - res = app.post_json_api(url_collection_search, payload) - - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 2 - assert len(res.json['data']) == 2 - actual_ids = self.get_ids(res.json['data']) - assert node_two._id in actual_ids - assert registration_two._id in actual_ids - - payload = self.post_payload(status='asdf', issue='0', volume='1', programArea='asdf', collectedType='') - res = app.post_json_api(url_collection_search, payload) - - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 1 - assert len(res.json['data']) == 1 - actual_ids = self.get_ids(res.json['data']) - assert node_one._id in actual_ids - - payload = self.post_payload(gradeLevels='super') - res = app.post_json_api(url_collection_search, payload) - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 1 - - # test_search_abstract_keyword_and_filter - payload = self.post_payload(q='Khadja', status='asdf') - res = app.post_json_api(url_collection_search, payload) - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 2 - assert len(res.json['data']) == 2 - actual_ids = self.get_ids(res.json['data']) - assert node_with_abstract._id in actual_ids - assert reg_with_abstract._id in actual_ids - - # test_search_abstract_keyword_and_filter_provider - payload = self.post_payload(q='Khadja', status='asdf', provider=collection_public.provider._id) - res = app.post_json_api(url_collection_search, payload) - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 1 - assert len(res.json['data']) == 1 - assert res.json['data'][0]['id'] == node_with_abstract._id - - def test_POST_search_collections_disease_data_type( - self, app, url_collection_search, user, node_one, node_two, collection_public, - node_with_abstract, node_private, registration_collection, registration_one, - registration_two, registration_private, reg_with_abstract): - - with capture_notifications(): - collection_public.collect_object(node_one, user, disease='illness', data_type='realness') - collection_public.collect_object(node_two, user, data_type='realness') - - payload = self.post_payload(disease='illness') - res = app.post_json_api(url_collection_search, payload) - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 1 - - payload = self.post_payload(dataType='realness') - res = app.post_json_api(url_collection_search, payload) - assert res.status_code == 200 - assert res.json['links']['meta']['total'] == 2 - assert len(res.json['data']) == 2 - - def test_POST_search_collections_scope(self, app, url_collection_search, user): - payload = self.post_payload(q='Collection') - - token_invalid = CasResponse( - authenticated=True, - user=user._id, - attributes={'accessTokenScope': ['osf.full_read']} - ) - with mock.patch('framework.auth.cas.CasClient.profile', return_value=token_invalid): - res = app.post_json_api(url_collection_search, payload, auth='some-invalid-token', expect_errors=True, auth_type='jwt') - assert res.status_code == 403 - - token_valid = CasResponse( - authenticated=True, - user=user._id, - attributes={'accessTokenScope': ['osf.full_read', 'osf.full_write']} - ) - with mock.patch('framework.auth.cas.CasClient.profile', return_value=token_valid): - res = app.post_json_api(url_collection_search, payload, auth='some-valid-token', auth_type='jwt') - assert res.status_code == 200 diff --git a/framework/auth/oauth_scopes.py b/framework/auth/oauth_scopes.py index f3f060a2571..2d121d61d57 100644 --- a/framework/auth/oauth_scopes.py +++ b/framework/auth/oauth_scopes.py @@ -204,8 +204,6 @@ class CoreScopes: READ_COLLECTION_SUBMISSION = 'read_collection_submission' WRITE_COLLECTION_SUBMISSION = 'write_collection_submission' - ADVANCED_SEARCH = 'advanced_search' - class ComposedScopes: """ @@ -336,7 +334,6 @@ class ComposedScopes: CoreScopes.CEDAR_METADATA_RECORD_READ, CoreScopes.MEETINGS_READ, CoreScopes.INSTITUTION_READ, - CoreScopes.SEARCH, CoreScopes.SCOPES_READ, CoreScopes.USERS_MESSAGE_READ_EMAIL )\ @@ -362,7 +359,6 @@ class ComposedScopes: CoreScopes.WRITE_COLLECTION_SUBMISSION_ACTION, CoreScopes.WRITE_COLLECTION_SUBMISSION, CoreScopes.USERS_MESSAGE_WRITE_EMAIL, - CoreScopes.ADVANCED_SEARCH ) # Admin permissions- includes functionality not intended for third-party use diff --git a/osf_tests/test_elastic_search.py b/osf_tests/test_elastic_search.py index b6c20e5af4a..e21ed1f44b2 100644 --- a/osf_tests/test_elastic_search.py +++ b/osf_tests/test_elastic_search.py @@ -1015,101 +1015,6 @@ def test_tag_aggregation(self): assert doc['key'] in tags -@pytest.mark.enable_search -@pytest.mark.enable_enqueue_task -class TestAddContributor(OsfTestCase): - # Tests of the search.search_contributor method - - def setUp(self): - self.name1 = 'Roger1 Taylor1' - self.name2 = 'John2 Deacon2' - self.name3 = 'j\xc3\xb3ebert3 Smith3' - self.name4 = 'B\xc3\xb3bbert4 Jones4' - - with run_celery_tasks(): - super().setUp() - self.user = factories.UserFactory(fullname=self.name1) - self.user3 = factories.UserFactory(fullname=self.name3) - - def test_unreg_users_dont_show_in_search(self): - unreg = factories.UnregUserFactory() - contribs = search.search_contributor(unreg.fullname) - assert len(contribs['users']) == 0 - - def test_unreg_users_do_show_on_projects(self): - with run_celery_tasks(): - unreg = factories.UnregUserFactory(fullname='Robert Paulson') - self.project = factories.ProjectFactory( - title='Glamour Rock', - creator=unreg, - is_public=True, - ) - results = query(unreg.fullname)['results'] - assert len(results) == 1 - - def test_search_fullname(self): - # Searching for full name yields exactly one result. - contribs = search.search_contributor(self.name1) - assert len(contribs['users']) == 1 - - contribs = search.search_contributor(self.name2) - assert len(contribs['users']) == 0 - - def test_search_firstname(self): - # Searching for first name yields exactly one result. - contribs = search.search_contributor(self.name1.split(' ')[0]) - assert len(contribs['users']) == 1 - - contribs = search.search_contributor(self.name2.split(' ')[0]) - assert len(contribs['users']) == 0 - - def test_search_partial(self): - # Searching for part of first name yields exactly one - # result. - contribs = search.search_contributor(self.name1.split(' ')[0][:-1]) - assert len(contribs['users']) == 1 - - contribs = search.search_contributor(self.name2.split(' ')[0][:-1]) - assert len(contribs['users']) == 0 - - def test_search_fullname_special_character(self): - # Searching for a fullname with a special character yields - # exactly one result. - contribs = search.search_contributor(self.name3) - assert len(contribs['users']) == 1 - - contribs = search.search_contributor(self.name4) - assert len(contribs['users']) == 0 - - def test_search_firstname_special_character(self): - # Searching for a first name with a special character yields - # exactly one result. - contribs = search.search_contributor(self.name3.split(' ')[0]) - assert len(contribs['users']) == 1 - - contribs = search.search_contributor(self.name4.split(' ')[0]) - assert len(contribs['users']) == 0 - - def test_search_partial_special_character(self): - # Searching for a partial name with a special character yields - # exactly one result. - contribs = search.search_contributor(self.name3.split(' ')[0][:-1]) - assert len(contribs['users']) == 1 - - contribs = search.search_contributor(self.name4.split(' ')[0][:-1]) - assert len(contribs['users']) == 0 - - def test_search_profile(self): - orcid = '123456' - user = factories.UserFactory() - user.social['orcid'] = orcid - user.save() - contribs = search.search_contributor(orcid) - assert len(contribs['users']) == 1 - assert len(contribs['users'][0]['social']) == 1 - assert contribs['users'][0]['social']['orcid'] == user.social_links['orcid'] - - @pytest.mark.enable_search @pytest.mark.enable_enqueue_task class TestProjectSearchResults(OsfTestCase): diff --git a/osf_tests/test_search_views.py b/osf_tests/test_search_views.py deleted file mode 100644 index 858decb9d1a..00000000000 --- a/osf_tests/test_search_views.py +++ /dev/null @@ -1,344 +0,0 @@ -from os import environ -from unittest import mock - -import pytest - -from osf_tests import factories -from tests.base import OsfTestCase -from website.util import api_url_for -from website.views import find_bookmark_collection -from osf.external.spam import tasks as spam_tasks - - -@pytest.mark.enable_search -@pytest.mark.enable_enqueue_task -class TestSearchViews(OsfTestCase): - - def setUp(self): - super().setUp() - import website.search.search as search - search.delete_all() - - robbie = factories.UserFactory(fullname='Robbie Williams') - self.project = factories.ProjectFactory(creator=robbie) - self.contrib = factories.UserFactory(fullname='Brian May') - for i in range(0, 12): - factories.UserFactory(fullname=f'Freddie Mercury{i}') - - self.user_one = factories.AuthUserFactory() - self.user_two = factories.AuthUserFactory() - self.project_private_user_one = factories.ProjectFactory(title='aaa', creator=self.user_one, is_public=False) - self.project_private_user_two = factories.ProjectFactory(title='aaa', creator=self.user_two, is_public=False) - self.project_public_user_one = factories.ProjectFactory(title='aaa', creator=self.user_one, is_public=True) - self.project_public_user_two = factories.ProjectFactory(title='aaa', creator=self.user_two, is_public=True) - - def tearDown(self): - super().tearDown() - import website.search.search as search - search.delete_all() - - # TODO: this might be failing b/c celery fails to propagate the changes to share - # see https://github.com/CenterForOpenScience/osf.io/pull/10599 - @pytest.mark.skipif( - not environ.get('CI'), - reason="for some reason elasticsearch environment isn't properly set up locally, so this test passes only in CI" - ) - def test_search_views(self): - # Test search contributor - url = api_url_for('search_contributor') - res = self.app.get(url, query_string={'query': self.contrib.fullname}) - assert res.status_code == 200 - result = res.json['users'] - assert len(result) == 1 - brian = result[0] - assert brian['fullname'] == self.contrib.fullname - assert 'profile_image_url' in brian - assert brian['registered'] == self.contrib.is_registered - assert brian['active'] == self.contrib.is_active - - # Test search pagination - res = self.app.get(url, query_string={'query': 'fr'}) - assert res.status_code == 200 - result = res.json['users'] - pages = res.json['pages'] - page = res.json['page'] - assert len(result) == 5 - assert pages == 3 - assert page == 0 - - # Test default page 1 - res = self.app.get(url, query_string={'query': 'fr', 'page': 1}) - assert res.status_code == 200 - result = res.json['users'] - page = res.json['page'] - assert len(result) == 5 - assert page == 1 - - # Test default page 2 - res = self.app.get(url, query_string={'query': 'fr', 'page': 2}) - assert res.status_code == 200 - result = res.json['users'] - page = res.json['page'] - assert len(result) == 4 - assert page == 2 - - # Test smaller pages - res = self.app.get(url, query_string={'query': 'fr', 'size': 5}) - assert res.status_code == 200 - result = res.json['users'] - pages = res.json['pages'] - page = res.json['page'] - assert len(result) == 5 - assert page == 0 - assert pages == 3 - - # Test smaller pages page 2 - res = self.app.get(url, query_string={'query': 'fr', 'size': 5, 'page': 2}) - assert res.status_code == 200 - result = res.json['users'] - pages = res.json['pages'] - page = res.json['page'] - assert len(result) == 4 - assert page == 2 - assert pages == 3 - - # Test search projects - url = '/search/' - res = self.app.get(url, query_string={'q': self.project.title}) - assert res.status_code == 200 - - # Test search node - res = self.app.post( - api_url_for('search_node'), - json={'query': self.project.title}, - auth=factories.AuthUserFactory().auth - ) - assert res.status_code == 200 - - # Test search node includePublic true - res = self.app.post( - api_url_for('search_node'), - json={'query': 'a', 'includePublic': True}, - auth=self.user_one.auth - ) - node_ids = [node['id'] for node in res.json['nodes']] - assert self.project_private_user_one._id in node_ids - assert self.project_public_user_one._id in node_ids - assert self.project_public_user_two._id in node_ids - assert self.project_private_user_two._id not in node_ids - - # Test search node includePublic false - res = self.app.post( - api_url_for('search_node'), - json={'query': 'a', 'includePublic': False}, - auth=self.user_one.auth - ) - node_ids = [node['id'] for node in res.json['nodes']] - assert self.project_private_user_one._id in node_ids - assert self.project_public_user_one._id in node_ids - assert self.project_public_user_two._id not in node_ids - assert self.project_private_user_two._id not in node_ids - - # Test search user - url = '/api/v1/search/user/' - res = self.app.get(url, query_string={'q': 'Umwali'}) - assert res.status_code == 200 - assert not res.json['results'] - - user_one = factories.AuthUserFactory(fullname='Joe Umwali') - user_two = factories.AuthUserFactory(fullname='Joan Uwase') - - res = self.app.get(url, query_string={'q': 'Umwali'}) - - assert res.status_code == 200 - assert len(res.json['results']) == 1 - assert not res.json['results'][0]['social'] - - user_one.social = { - 'github': user_one.given_name, - 'twitter': user_one.given_name, - 'ssrn': user_one.given_name - } - user_one.save() - - res = self.app.get(url, query_string={'q': 'Umwali'}) - - assert res.status_code == 200 - assert len(res.json['results']) == 1 - assert 'Joan' not in res.text - assert res.json['results'][0]['social'] - assert res.json['results'][0]['names']['fullname'] == user_one.fullname - assert res.json['results'][0]['social']['github'] == f'http://github.com/{user_one.given_name}' - assert res.json['results'][0]['social']['twitter'] == f'http://twitter.com/{user_one.given_name}' - assert res.json['results'][0]['social']['ssrn'] == f'http://papers.ssrn.com/sol3/cf_dev/AbsByAuth.cfm?per_id={user_one.given_name}' - - user_two.social = { - 'profileWebsites': [f'http://me.com/{user_two.given_name}'], - 'orcid': user_two.given_name, - 'linkedIn': user_two.given_name, - 'scholar': user_two.given_name, - 'impactStory': user_two.given_name, - 'baiduScholar': user_two.given_name - } - with mock.patch.object(spam_tasks.requests, 'head'): - user_two.save() - - user_three = factories.AuthUserFactory(fullname='Janet Umwali') - user_three.social = { - 'github': user_three.given_name, - 'ssrn': user_three.given_name - } - user_three.save() - - res = self.app.get(url, query_string={'q': 'Umwali'}) - - assert res.status_code == 200 - assert len(res.json['results']) == 2 - assert res.json['results'][0]['social'] - assert res.json['results'][1]['social'] - assert res.json['results'][0]['social']['ssrn'] != res.json['results'][1]['social']['ssrn'] - assert res.json['results'][0]['social']['github'] != res.json['results'][1]['social']['github'] - - res = self.app.get(url, query_string={'q': 'Uwase'}) - - assert res.status_code == 200 - assert len(res.json['results']) == 1 - assert res.json['results'][0]['social'] - assert 'ssrn' not in res.json['results'][0]['social'] - assert res.json['results'][0]['social']['profileWebsites'][0] == f'http://me.com/{user_two.given_name}' - assert res.json['results'][0]['social']['impactStory'] == f'https://impactstory.org/u/{user_two.given_name}' - assert res.json['results'][0]['social']['orcid'] == f'http://orcid.org/{user_two.given_name}' - assert res.json['results'][0]['social']['baiduScholar'] == f'http://xueshu.baidu.com/scholarID/{user_two.given_name}' - assert res.json['results'][0]['social']['linkedIn'] == f'https://www.linkedin.com/{user_two.given_name}' - assert res.json['results'][0]['social']['scholar'] == f'http://scholar.google.com/citations?user={user_two.given_name}' - - -@pytest.mark.enable_bookmark_creation -class TestODMTitleSearch(OsfTestCase): - """ Docs from original method: - :arg term: The substring of the title. - :arg category: Category of the node. - :arg isDeleted: yes, no, or either. Either will not add a qualifier for that argument in the search. - :arg isFolder: yes, no, or either. Either will not add a qualifier for that argument in the search. - :arg isRegistration: yes, no, or either. Either will not add a qualifier for that argument in the search. - :arg includePublic: yes or no. Whether the projects listed should include public projects. - :arg includeContributed: yes or no. Whether the search should include projects the current user has - contributed to. - :arg ignoreNode: a list of nodes that should not be included in the search. - :return: a list of dictionaries of projects - """ - - def setUp(self): - super().setUp() - - self.user = factories.AuthUserFactory() - self.user_two = factories.AuthUserFactory() - self.project = factories.ProjectFactory(creator=self.user, title='foo') - self.project_two = factories.ProjectFactory(creator=self.user_two, title='bar') - self.public_project = factories.ProjectFactory(creator=self.user_two, is_public=True, title='baz') - self.registration_project = factories.RegistrationFactory(creator=self.user, title='qux') - self.folder = factories.CollectionFactory(creator=self.user, title='quux') - self.dashboard = find_bookmark_collection(self.user) - self.url = api_url_for('search_projects_by_title') - - def test_search_projects_by_title(self): - res = self.app.get(self.url, query_string={'term': self.project.title}, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 1 - res = self.app.get(self.url, - query_string={ - 'term': self.public_project.title, - 'includePublic': 'yes', - 'includeContributed': 'no' - }, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 1 - res = self.app.get(self.url, - query_string={ - 'term': self.project.title, - 'includePublic': 'no', - 'includeContributed': 'yes' - }, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 1 - res = self.app.get(self.url, - query_string={ - 'term': self.project.title, - 'includePublic': 'no', - 'includeContributed': 'yes', - 'isRegistration': 'no' - }, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 1 - res = self.app.get(self.url, - query_string={ - 'term': self.project.title, - 'includePublic': 'yes', - 'includeContributed': 'yes', - 'isRegistration': 'either' - }, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 1 - res = self.app.get(self.url, - query_string={ - 'term': self.public_project.title, - 'includePublic': 'yes', - 'includeContributed': 'yes', - 'isRegistration': 'either' - }, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 1 - res = self.app.get(self.url, - query_string={ - 'term': self.registration_project.title, - 'includePublic': 'yes', - 'includeContributed': 'yes', - 'isRegistration': 'either' - }, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 2 - res = self.app.get(self.url, - query_string={ - 'term': self.registration_project.title, - 'includePublic': 'yes', - 'includeContributed': 'yes', - 'isRegistration': 'no' - }, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 1 - res = self.app.get(self.url, - query_string={ - 'term': self.folder.title, - 'includePublic': 'yes', - 'includeContributed': 'yes', - 'isFolder': 'yes' - }, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 0 - res = self.app.get(self.url, - query_string={ - 'term': self.folder.title, - 'includePublic': 'yes', - 'includeContributed': 'yes', - 'isFolder': 'no' - }, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 0 - res = self.app.get(self.url, - query_string={ - 'term': self.dashboard.title, - 'includePublic': 'yes', - 'includeContributed': 'yes', - 'isFolder': 'no' - }, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 0 - res = self.app.get(self.url, - query_string={ - 'term': self.dashboard.title, - 'includePublic': 'yes', - 'includeContributed': 'yes', - 'isFolder': 'yes' - }, auth=self.user.auth) - assert res.status_code == 200 - assert len(res.json) == 0 diff --git a/tests/test_misc_views.py b/tests/test_misc_views.py index 3d1e0cad47c..989062ac77a 100644 --- a/tests/test_misc_views.py +++ b/tests/test_misc_views.py @@ -354,13 +354,6 @@ def test_get_node_parent_not_admin(self): assert children[0]['node']['id'] == child3._primary_key -class TestPublicViews(OsfTestCase): - - def test_explore(self): - res = self.app.get('/explore/', follow_redirects=True) - assert res.status_code == 200 - - class TestExternalAuthViews(OsfTestCase): def setUp(self): diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 686c7a4c8ea..f7d9b47037e 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -1,4 +1,3 @@ -from unittest import mock import datetime as dt import pytest @@ -11,11 +10,11 @@ ) from osf.models import NodeRelation from osf.utils import permissions -from tests.base import OsfTestCase, get_default_metaschema +from tests.base import OsfTestCase from framework.auth import Auth from tests.utils import capture_notifications -from website.project.views.node import _view_project, _serialize_node_search, _get_children, _get_readable_descendants +from website.project.views.node import _view_project, _get_children, _get_readable_descendants from website.views import serialize_node_summary from website.profile import utils from website import filters, settings @@ -189,16 +188,6 @@ def test_serialize_node_summary_child_exists(self): result = _view_project(parent_node, Auth(user)) assert result['node']['child_exists'] == True - def test_serialize_node_search_returns_only_visible_contributors(self): - node = NodeFactory() - non_visible_contributor = UserFactory() - node.add_contributor(non_visible_contributor, visible=False) - serialized_node = _serialize_node_search(node) - - assert serialized_node['firstAuthor'] == node.visible_contributors[0].family_name - assert len(node.visible_contributors) == 1 - assert not serialized_node['etal'] - @pytest.mark.enable_bookmark_creation class TestViewProject(OsfTestCase): diff --git a/webpack.common.config.js b/webpack.common.config.js index e76b755dbdf..811645937c8 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -46,7 +46,6 @@ var entry = { 'conference-page': staticPath('js/pages/conference-page.js'), 'meetings-page': staticPath('js/pages/meetings-page.js'), 'view-file-tree-page': staticPath('js/pages/view-file-tree-page.js'), - 'search-page': staticPath('js/pages/search-page.js'), 'profile-settings-addons-page': staticPath('js/pages/profile-settings-addons-page.js'), 'forgotpassword-page': staticPath('js/pages/forgotpassword-page.js'), 'resetpassword-page': staticPath('js/pages/resetpassword-page.js'), diff --git a/website/discovery/__init__.py b/website/discovery/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/website/discovery/views.py b/website/discovery/views.py deleted file mode 100644 index c17c5ca0e7a..00000000000 --- a/website/discovery/views.py +++ /dev/null @@ -1,5 +0,0 @@ -from framework.flask import redirect - - -def redirect_activity_to_search(**kwargs): - return redirect('/search/') diff --git a/website/language.py b/website/language.py index 8f292955680..024783d13ec 100644 --- a/website/language.py +++ b/website/language.py @@ -100,8 +100,6 @@ # ########### # Search-related errors -SEARCH_QUERY_HELP = ('Please check our help (the question mark beside the search box) for more information ' - 'on advanced search queries.') # Shown at error page if an expired/revokes email confirmation link is clicked EXPIRED_EMAIL_CONFIRM_TOKEN = 'This confirmation link has expired. Please log in to continue.' diff --git a/website/project/views/node.py b/website/project/views/node.py index 94e4a63fe47..9315eb59065 100644 --- a/website/project/views/node.py +++ b/website/project/views/node.py @@ -1,7 +1,6 @@ import os import logging from rest_framework import status as http_status -import math from collections import defaultdict import waffle @@ -9,7 +8,7 @@ from django.apps import apps from django.utils import timezone from django.core.exceptions import ValidationError -from django.db.models import Q, OuterRef, Subquery +from django.db.models import OuterRef, Subquery from framework import status from framework.forms import push_errors_to_status @@ -53,7 +52,7 @@ from osf.utils.permissions import ADMIN, READ, WRITE, CREATOR_PERMISSIONS, ADMIN_NODE from osf.utils.workflows import CollectionSubmissionStates from website import settings -from website.views import find_bookmark_collection, validate_page_num +from website.views import find_bookmark_collection from website.views import serialize_node_summary, get_storage_region_list from website.profile import utils from addons.mendeley.provider import MendeleyCitationsProvider @@ -1229,77 +1228,6 @@ def project_private_link_edit(auth, **kwargs): ) -def _serialize_node_search(node): - """Serialize a node for use in pointer search. - - :param Node node: Node to serialize - :return: Dictionary of node data - - """ - data = { - 'id': node._id, - 'title': node.title, - 'etal': len(node.visible_contributors) > 1, - 'isRegistration': node.is_registration - } - if node.is_registration: - data['title'] += ' (registration)' - data['dateRegistered'] = node.registered_date.isoformat() - else: - data['dateCreated'] = node.created.isoformat() - data['dateModified'] = node.modified.isoformat() - - first_author = node.visible_contributors[0] - data['firstAuthor'] = first_author.family_name or first_author.given_name or first_author.fullname - - return data - - -@must_be_logged_in -def search_node(auth, **kwargs): - """ - - """ - # Get arguments - node = AbstractNode.load(request.json.get('nodeId')) - include_public = request.json.get('includePublic') - size = float(request.json.get('size', '5').strip()) - page = request.json.get('page', 0) - query = request.json.get('query', '').strip() - - start = (page * size) - if not query: - return {'nodes': []} - - # Exclude current node from query if provided - nin = [node.id] + list(node._nodes.values_list('pk', flat=True)) if node else [] - - can_view_query = Q(_contributors=auth.user) - if include_public: - can_view_query = can_view_query | Q(is_public=True) - - nodes = (AbstractNode.objects.filter( - can_view_query, - title__icontains=query, - is_deleted=False - ).exclude(id__in=nin).exclude(type='osf.collection')) - - count = nodes.count() - pages = math.ceil(count / size) - validate_page_num(page, pages) - - return { - 'nodes': [ - _serialize_node_search(each) - for each in nodes[start: start + size] - if each.contributors - ], - 'total': count, - 'pages': pages, - 'page': page - } - - def _add_pointers(node, pointers, auth): """ diff --git a/website/routes.py b/website/routes.py index 226f03fb1f1..58b5f78ddb6 100644 --- a/website/routes.py +++ b/website/routes.py @@ -38,14 +38,12 @@ from website import landing_pages as landing_page_views from website import views as website_views from website.citations import views as citation_views -from website.search import views as search_views from website.oauth import views as oauth_views from addons.osfstorage import views as osfstorage_views from website.profile.utils import get_profile_image_url from website.profile import views as profile_views from website.project import views as project_views from addons.base import views as addon_views -from website.discovery import views as discovery_views from website.conferences import views as conference_views from website.policies import views as policy_views from website.preprints import views as preprint_views @@ -524,17 +522,6 @@ def make_url_map(app): Rule('/forms/forgot_password/', 'get', website_views.forgot_password_form, json_renderer), ], prefix='/api/v1') - ### Discovery ### - - process_rules(app, [ - Rule( - ['/activity/', '/explore/activity/', '/explore/'], - 'get', - discovery_views.redirect_activity_to_search, - notemplate - ), - ]) - ### Auth ### process_rules(app, [ @@ -943,48 +930,19 @@ def make_url_map(app): ], prefix='/api/v1',) - ### Search ### - # Web process_rules(app, [ - Rule( - '/search/', - 'get', - search_views.search_view, - OsfWebRenderer('search.mako', trust=False) - ), Rule( '/share/registration/', 'get', {'register': settings.SHARE_REGISTRATION_URL}, json_renderer ), - Rule( - '/api/v1/user/search/', - 'get', search_views.search_contributor, - json_renderer - ), - Rule( - '/api/v1/search/node/', - 'post', - project_views.node.search_node, - json_renderer, - ), ]) - # API - - process_rules(app, [ - - Rule(['/search/', '/search//'], ['get', 'post'], search_views.search_search, json_renderer), - Rule('/search/projects/', 'get', search_views.search_projects_by_title, json_renderer), - Rule('/share/search/', 'get', website_views.legacy_share_v1_search, json_renderer), - - ], prefix='/api/v1') - # Web process_rules(app, [ diff --git a/website/search/elastic_search.py b/website/search/elastic_search.py index 107feda2010..198bddc30cb 100644 --- a/website/search/elastic_search.py +++ b/website/search/elastic_search.py @@ -1,8 +1,6 @@ import copy import functools import logging -import math -import re import unicodedata from framework import sentry @@ -15,22 +13,16 @@ from framework.celery_tasks import app as celery_app from framework.database import paginated from osf.models import AbstractNode -from osf.models import OSFUser -from osf.models import BaseFileNode from osf.models import GuidMetadataRecord -from osf.models import Institution from osf.models import Preprint from osf.models import SpamStatus from addons.wiki.models import WikiPage -from osf.models import CollectionSubmission from osf.utils.sanitize import unescape_entities from osf.utils.workflows import CollectionSubmissionStates from website import settings -from website.filters import profile_image_url from osf.models.licenses import serialize_node_license_record from website.search import exceptions -from website.search.util import build_query, clean_splitters -from website.views import validate_page_num +from website.search.util import clean_splitters logger = logging.getLogger(__name__) @@ -48,16 +40,6 @@ 'group': 'Groups', } -DOC_TYPE_TO_MODEL = { - 'component': AbstractNode, - 'project': AbstractNode, - 'registration': AbstractNode, - 'user': OSFUser, - 'file': BaseFileNode, - 'institution': Institution, - 'preprint': Preprint, - 'collectionSubmission': CollectionSubmission, -} # Prevent tokenizing and stop word removal. NOT_ANALYZED_PROPERTY = {'type': 'string', 'index': 'not_analyzed'} @@ -886,86 +868,6 @@ def delete_group_doc(deleted_id, index=None): index = index or INDEX client().delete(index=index, doc_type='group', id=deleted_id, refresh=True, ignore=[404]) -@requires_search -def search_contributor(query, page=0, size=10, exclude=None, current_user=None): - """Search for contributors to add to a project using elastic search. Request must - include JSON data with a "query" field. - - :param query: The substring of the username to search for - :param page: For pagination, the page number to use for results - :param size: For pagination, the number of results per page - :param exclude: A list of User objects to exclude from the search - :param current_user: A User object of the current user - - :return: List of dictionaries, each containing the ID, full name, - most recent employment and education, profile_image URL of an OSF user - - """ - start = (page * size) - items = re.split(r'[\s-]+', query) - exclude = exclude or [] - normalized_items = [] - for item in items: - normalized_item = unicodedata.normalize('NFKD', item) - normalized_items.append(normalized_item) - items = normalized_items - - query = ' AND '.join(f'{re.escape(item)}*~' for item in items) + \ - ''.join(f' NOT id:"{excluded._id}"' for excluded in exclude) - - results = search(build_query(query, start=start, size=size), index=INDEX, doc_type='user') - docs = results['results'] - pages = math.ceil(results['counts'].get('user', 0) / size) - validate_page_num(page, pages) - - users = [] - for doc in docs: - # TODO: use utils.serialize_user - user = OSFUser.load(doc['id']) - - if current_user and current_user._id == user._id: - n_projects_in_common = -1 - elif current_user: - n_projects_in_common = current_user.n_projects_in_common(user) - else: - n_projects_in_common = 0 - - if user is None: - logger.error(f"Could not load user {doc['id']}") - continue - if user.is_active: # exclude merged, unregistered, etc. - current_employment = None - education = None - - if user.jobs: - current_employment = user.jobs[0]['institution'] - - if user.schools: - education = user.schools[0]['institution'] - - users.append({ - 'fullname': doc['user'], - 'id': doc['id'], - 'employment': current_employment, - 'education': education, - 'social': user.social_links, - 'n_projects_in_common': n_projects_in_common, - 'profile_image_url': profile_image_url(settings.PROFILE_IMAGE_PROVIDER, - user, - use_ssl=True, - size=settings.PROFILE_IMAGE_MEDIUM), - 'profile_url': user.profile_url, - 'registered': user.is_registered, - 'active': user.is_active - }) - - return { - 'users': users, - 'total': results['counts']['total'], - 'pages': pages, - 'page': page, - } - def serialize_guid_metadata(guid): serialized_guid_metadata = {} diff --git a/website/search/search.py b/website/search/search.py index 4632ad0b3dd..d2a3af1b58b 100644 --- a/website/search/search.py +++ b/website/search/search.py @@ -159,11 +159,3 @@ def delete_index(index): def create_index(index=None): index = index or settings.ELASTIC_INDEX search_engine.create_index(index=index) - - -@requires_search -def search_contributor(query, page=0, size=10, exclude=None, current_user=None): - exclude = exclude or [] - result = search_engine.search_contributor(query=query, page=page, size=size, - exclude=exclude, current_user=current_user) - return result diff --git a/website/search/views.py b/website/search/views.py deleted file mode 100644 index 828cd0e5661..00000000000 --- a/website/search/views.py +++ /dev/null @@ -1,202 +0,0 @@ -import functools -from rest_framework import status as http_status -import logging -import time - -from django.db.models import Q -from flask import request - -from framework.auth.decorators import collect_auth -from framework.auth.decorators import must_be_logged_in -from framework.exceptions import HTTPError -from framework import sentry -from framework.utils import sanitize_html -from website import language -from osf.models import OSFUser, AbstractNode -from website import settings -from website.project.views.contributor import get_node_contributors_abbrev -from website.search import exceptions -import website.search.search as search -from website.search.util import build_query - -logger = logging.getLogger(__name__) - -RESULTS_PER_PAGE = 250 - - -def handle_search_errors(func): - @functools.wraps(func) - def wrapped(*args, **kwargs): - try: - return func(*args, **kwargs) - except exceptions.MalformedQueryError: - raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data={ - 'message_short': 'Bad search query', - 'message_long': language.SEARCH_QUERY_HELP, - }) - except exceptions.SearchUnavailableError: - raise HTTPError(http_status.HTTP_503_SERVICE_UNAVAILABLE, data={ - 'message_short': 'Search unavailable', - 'message_long': ('Our search service is currently unavailable, if the issue persists, ' - + language.SUPPORT_LINK), - }) - except exceptions.SearchException as e: - # Interim fix for issue where ES fails with 500 in some settings- ensure exception is still logged until it can be better debugged. See OSF-4538 - sentry.log_exception(e) - sentry.log_message('Elasticsearch returned an unexpected error response') - # TODO: Add a test; may need to mock out the error response due to inability to reproduce error code locally - raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data={ - 'message_short': 'Could not perform search query', - 'message_long': language.SEARCH_QUERY_HELP, - }) - return wrapped - - -@handle_search_errors -def search_search(**kwargs): - _type = kwargs.get('type', None) - - tick = time.time() - results = {} - - if request.method == 'POST': - results = search.search(request.get_json(), doc_type=_type) - elif request.method == 'GET': - q = request.args.get('q', '*') - # TODO Match javascript params? - start = request.args.get('from', '0') - size = request.args.get('size', '10') - results = search.search(build_query(q, start, size), doc_type=_type) - - results['time'] = round(time.time() - tick, 2) - return results - -def search_view(): - return {'shareUrl': settings.SHARE_URL}, - -def conditionally_add_query_item(query, item, condition, value): - """ Helper for the search_projects_by_title function which will add a condition to a query - It will give an error if the proper search term is not used. - :param query: The modular ODM query that you want to modify - :param item: the field to query on - :param condition: yes, no, or either - :return: the modified query - """ - - condition = condition.lower() - - if condition == 'yes': - return query & Q(**{item: value}) - elif condition == 'no': - return query & ~Q(**{item: value}) - elif condition == 'either': - return query - - raise HTTPError(http_status.HTTP_400_BAD_REQUEST) - - -@must_be_logged_in -def search_projects_by_title(**kwargs): - """ Search for nodes by title. Can pass in arguments from the URL to modify the search - :arg term: The substring of the title. - :arg category: Category of the node. - :arg isDeleted: yes, no, or either. Either will not add a qualifier for that argument in the search. - :arg isFolder: yes, no, or either. Either will not add a qualifier for that argument in the search. - :arg isRegistration: yes, no, or either. Either will not add a qualifier for that argument in the search. - :arg includePublic: yes or no. Whether the projects listed should include public projects. - :arg includeContributed: yes or no. Whether the search should include projects the current user has - contributed to. - :arg ignoreNode: a list of nodes that should not be included in the search. - :return: a list of dictionaries of projects - - """ - # TODO(fabianvf): At some point, it would be nice to do this with elastic search - user = kwargs['auth'].user - - term = request.args.get('term', '') - max_results = int(request.args.get('maxResults', '10')) - category = request.args.get('category', 'project').lower() - is_deleted = request.args.get('isDeleted', 'no').lower() - is_collection = request.args.get('isFolder', 'no').lower() - is_registration = request.args.get('isRegistration', 'no').lower() - include_public = request.args.get('includePublic', 'yes').lower() - include_contributed = request.args.get('includeContributed', 'yes').lower() - ignore_nodes = request.args.getlist('ignoreNode', []) - - matching_title = Q( - title__icontains=term, # search term (case-insensitive) - category=category # is a project - ) - - matching_title = conditionally_add_query_item(matching_title, 'is_deleted', is_deleted, True) - matching_title = conditionally_add_query_item(matching_title, 'type', is_registration, 'osf.registration') - matching_title = conditionally_add_query_item(matching_title, 'type', is_collection, 'osf.collection') - - if len(ignore_nodes) > 0: - for node_id in ignore_nodes: - matching_title = matching_title & ~Q(_id=node_id) - - my_projects = [] - my_project_count = 0 - public_projects = [] - - if include_contributed == 'yes': - my_projects = AbstractNode.objects.filter( - matching_title & - Q(_contributors=user) # user is a contributor - )[:max_results] - my_project_count = my_project_count - - if my_project_count < max_results and include_public == 'yes': - public_projects = AbstractNode.objects.filter( - matching_title & - Q(is_public=True) # is public - )[:max_results - my_project_count] - - results = list(my_projects) + list(public_projects) - ret = process_project_search_results(results, **kwargs) - return ret - - -@must_be_logged_in -def process_project_search_results(results, **kwargs): - """ - :param results: list of projects from the modular ODM search - :return: we return the entire search result, which is a list of - dictionaries. This includes the list of contributors. - """ - user = kwargs['auth'].user - - ret = [] - - for project in results: - authors = get_node_contributors_abbrev(project=project, auth=kwargs['auth']) - authors_html = '' - for author in authors['contributors']: - a = OSFUser.load(author['user_id']) - authors_html += f'{a.fullname}' - authors_html += author['separator'] + ' ' - authors_html += ' ' + authors['others_count'] - - ret.append({ - 'id': project._id, - 'label': project.title, - 'value': project.title, - 'category': 'My Projects' if user in project.contributors else 'Public Projects', - 'authors': authors_html, - }) - - return ret - - -@collect_auth -def search_contributor(auth): - user = auth.user if auth else None - nid = request.args.get('excludeNode') - exclude = AbstractNode.load(nid).contributors if nid else [] - # TODO: Determine whether bleach is appropriate for ES payload. Also, inconsistent with website.sanitize.util.strip_html - query = sanitize_html(request.args.get('query', ''), tags=set(), strip=True) - page = int(sanitize_html(request.args.get('page', '0'), tags=set(), strip=True)) - size = int(sanitize_html(request.args.get('size', '5'), tags=set(), strip=True)) - return search.search_contributor(query=query, page=page, size=size, - exclude=exclude, current_user=user) diff --git a/website/static/css/pages/search-page.css b/website/static/css/pages/search-page.css deleted file mode 100644 index 4369eebabf5..00000000000 --- a/website/static/css/pages/search-page.css +++ /dev/null @@ -1,112 +0,0 @@ -.add-query -{ - width: 190px; - margin-left: 10px; - display: inline-block; - font-weight: normal; -} -.navigate -{ - padding:25px; - font-size: 18px; -} -.remove-button -{ - color: #ffffff; -} -.search-types a -{ - font-size: 16px; -} - -.registration a { - color: #666; -} - -.registration small{ - color: #000000; -} - -.registration { - background-color: #AAA; - color: #ffffff; -} - -.query-label -{ - margin-right: .5em; -} -.result -{ - margin-bottom: 10px; - overflow: hidden; -} -.title h4 -{ - padding: 0; - padding-bottom: 5px; - margin: 0; -} -.private-title -{ - font-weight: normal; - font-style: italic; -} -.search-field -{ - padding: 0; - padding-top: 2px; - padding-bottom: 2px; - margin: 0; - line-height: 16px; -} -.search-field p -{ - margin: 0; -} -.description h5 -{ - padding: 0; - padding-bottom: 4px; - margin: 0; - font-weight: normal; - line-height: 16px; -} -.search-tags -{ - padding-top: 2px; - line-height: 24px; -} -.search-tags a -{ - margin-right: .5em; -} - -div.search-result { - margin-bottom: 10pt; - padding: 10px; - border-bottom: 1px solid #d3d3d3; - overflow-x: hidden; -} - -.search-results.search-result h5 small{ - color: white; - font-size: 10pt; -} - - -.search-results.search-result { - overflow-x: hidden; - -} -/* Search Contributors */ -.search-contributor-result td { - padding-right: 10px; -} -.search-contributor-links { - padding-top: 10px; -} - -#searchControls .osf-search { - z-index: 1029; /* No larger than 1030 */ -} diff --git a/website/static/css/search-bar.css b/website/static/css/search-bar.css deleted file mode 100644 index ef52190258f..00000000000 --- a/website/static/css/search-bar.css +++ /dev/null @@ -1,73 +0,0 @@ -/* Search bar */ -.search-label-placeholder { - position:absolute; - left: 0; - right: 0; - bottom: 0; - font-size: 20px; - color: #738EA2; - font-weight: 300; - visibility: hidden; -} - -.osf-search { - padding: 10px 0; - background: #B8ECC0; - position: fixed; - width: 100%; - box-shadow: 0 0 9px -2px #464545; - left: 0; - top: 50px; - z-index: 1030; /* Should not less than 1030 */ -} - -.osf-search-input { - background: none; - border: none; - box-shadow: none; - border-bottom: 1px dotted #FFF; - border-radius: 0; - padding: 0 0; - font-size: 20px; - color: #214762; - font-weight: 300; -} -.osf-search-input:focus { - outline : 0 !important; - box-shadow: none; - border-bottom: 1px dotted #FFF; -} -.osf-search-btn { - color: #214762; - background: rgba(0, 0, 0, 0); -} - -.osf-search-btn:hover { - color: #738EA2; -} - - -#searchPageFullBar::-webkit-input-placeholder { - color: #738EA2; -} - -#searchPageFullBar:-moz-placeholder { /* Firefox 18- */ - color: #738EA2; -} - -#searchPageFullBar::-moz-placeholder { /* Firefox 19+ */ - color: #738EA2; -} - -#searchPageFullBar:-ms-input-placeholder { - color: #738EA2; -} -#searchControls>.row { - padding-top: 60px; -} - -/*Moves maintenance alert under search bar*/ -#maintenance.alert { - position: relative; - top: 55px; -} diff --git a/website/static/js/contribAdder.js b/website/static/js/contribAdder.js index b7436c4d9d2..216a3a8797d 100644 --- a/website/static/js/contribAdder.js +++ b/website/static/js/contribAdder.js @@ -210,30 +210,10 @@ AddContributorViewModel = oop.extend(Paginator, { var self = this; self.doneSearching(false); self.notification(false); - if (self.query()) { - return $.getJSON( - '/api/v1/user/search/', { - query: self.query(), - page: self.pageToGet - }, - function (result) { - var contributors = result.users.map(function (userData) { - userData.added = (self.contributors().indexOf(userData.id) !== -1); - return new Contributor(userData); - }); - self.doneSearching(true); - self.results(contributors); - self.currentPage(result.page); - self.numberOfPages(result.pages); - self.addNewPaginators(false); - } - ); - } else { - self.results([]); - self.currentPage(0); - self.totalPages(0); - self.doneSearching(true); - } + self.results([]); + self.currentPage(0); + self.totalPages(0); + self.doneSearching(true); } }, getContributors: function () { diff --git a/website/static/js/pages/search-page.js b/website/static/js/pages/search-page.js deleted file mode 100644 index ca5144148c0..00000000000 --- a/website/static/js/pages/search-page.js +++ /dev/null @@ -1,6 +0,0 @@ -var $ = require('jquery'); -$('input[name=q]').remove(); - -var Search = require('../search.js'); -require('../../css/search-bar.css'); -new Search('#searchControls', '/api/v1/search/', ''); diff --git a/website/templates/search.mako b/website/templates/search.mako deleted file mode 100644 index 7961e5a80ee..00000000000 --- a/website/templates/search.mako +++ /dev/null @@ -1,393 +0,0 @@ -<%inherit file="base.mako"/> -<%def name="title()">Search -<%def name="stylesheets()"> - ${parent.stylesheets()} - - - -<%def name="content()"> -
- <%include file='./search_bar.mako' /> -
-
-
- -
-
-
- -
-
- -
-
-

Improve your search:

- - - - - - - - - - - - - - - - - - - - -
-
-
- - -
-
- -
- -
-
-
- - - - - - - - -
-
-
- - -
-
-
-
-
-
- - - - - - - - - - - - - -<%def name="javascript_bottom()"> - - - - - - diff --git a/website/views.py b/website/views.py index 1a4bf4942da..74a81f0501a 100644 --- a/website/views.py +++ b/website/views.py @@ -333,16 +333,6 @@ def redirect_to_registration_workflow(**kwargs): return redirect(furl(DOMAIN).add(path='registries/osf/new').url) -# Return error for legacy SHARE v1 search route -def legacy_share_v1_search(**kwargs): - return HTTPError( - http_status.HTTP_400_BAD_REQUEST, - data=dict( - message_long=f'Please use v2 of the SHARE search API available at {settings.SHARE_URL}api/v2/share/search/creativeworks/_search.' - ) - ) - - def get_storage_region_list(user, node=False): if not user: # Preserves legacy frontend test behavior return []