mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-01-03 05:48:44 +00:00
Search class views
This commit is contained in:
parent
beeeaaaf39
commit
b6bdfab943
6 changed files with 169 additions and 181 deletions
|
@ -1,17 +1,12 @@
|
||||||
''' test for app action functionality '''
|
''' test for app action functionality '''
|
||||||
import json
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.template.response import TemplateResponse
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm import vviews as views
|
from bookwyrm import vviews as views
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
from bookwyrm.settings import USER_AGENT
|
||||||
from bookwyrm.connectors import abstract_connector
|
|
||||||
from bookwyrm.settings import DOMAIN, USER_AGENT
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods
|
# pylint: disable=too-many-public-methods
|
||||||
|
@ -142,128 +137,6 @@ class Views(TestCase):
|
||||||
self.assertEqual(statuses[0], rat_mention)
|
self.assertEqual(statuses[0], rat_mention)
|
||||||
|
|
||||||
|
|
||||||
def test_search_json_response(self):
|
|
||||||
''' searches local data only and returns book data in json format '''
|
|
||||||
# we need a connector for this, sorry
|
|
||||||
request = self.factory.get('', {'q': 'Test Book'})
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
response = views.search(request)
|
|
||||||
self.assertIsInstance(response, JsonResponse)
|
|
||||||
|
|
||||||
data = json.loads(response.content)
|
|
||||||
self.assertEqual(len(data), 1)
|
|
||||||
self.assertEqual(data[0]['title'], 'Test Book')
|
|
||||||
self.assertEqual(
|
|
||||||
data[0]['key'], 'https://%s/book/%d' % (DOMAIN, self.book.id))
|
|
||||||
|
|
||||||
|
|
||||||
def test_search_html_response(self):
|
|
||||||
''' searches remote connectors '''
|
|
||||||
class TestConnector(abstract_connector.AbstractMinimalConnector):
|
|
||||||
''' nothing added here '''
|
|
||||||
def format_search_result(self, search_result):
|
|
||||||
pass
|
|
||||||
def get_or_create_book(self, remote_id):
|
|
||||||
pass
|
|
||||||
def parse_search_data(self, data):
|
|
||||||
pass
|
|
||||||
models.Connector.objects.create(
|
|
||||||
identifier='example.com',
|
|
||||||
connector_file='openlibrary',
|
|
||||||
base_url='https://example.com',
|
|
||||||
books_url='https://example.com/books',
|
|
||||||
covers_url='https://example.com/covers',
|
|
||||||
search_url='https://example.com/search?q=',
|
|
||||||
)
|
|
||||||
connector = TestConnector('example.com')
|
|
||||||
|
|
||||||
search_result = abstract_connector.SearchResult(
|
|
||||||
key='http://www.example.com/book/1',
|
|
||||||
title='Gideon the Ninth',
|
|
||||||
author='Tamsyn Muir',
|
|
||||||
year='2019',
|
|
||||||
connector=connector
|
|
||||||
)
|
|
||||||
|
|
||||||
request = self.factory.get('', {'q': 'Test Book'})
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
with patch(
|
|
||||||
'bookwyrm.connectors.connector_manager.search') as manager:
|
|
||||||
manager.return_value = [search_result]
|
|
||||||
response = views.search(request)
|
|
||||||
self.assertIsInstance(response, TemplateResponse)
|
|
||||||
self.assertEqual(response.template_name, 'search_results.html')
|
|
||||||
self.assertEqual(
|
|
||||||
response.context_data['book_results'][0].title, 'Gideon the Ninth')
|
|
||||||
|
|
||||||
|
|
||||||
def test_search_html_response_users(self):
|
|
||||||
''' searches remote connectors '''
|
|
||||||
request = self.factory.get('', {'q': 'mouse'})
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
with patch('bookwyrm.connectors.connector_manager.search'):
|
|
||||||
response = views.search(request)
|
|
||||||
self.assertIsInstance(response, TemplateResponse)
|
|
||||||
self.assertEqual(response.template_name, 'search_results.html')
|
|
||||||
self.assertEqual(
|
|
||||||
response.context_data['user_results'][0], self.local_user)
|
|
||||||
|
|
||||||
|
|
||||||
def test_tag_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
tag = models.Tag.objects.create(name='hi there')
|
|
||||||
models.UserTag.objects.create(
|
|
||||||
tag=tag, user=self.local_user, book=self.book)
|
|
||||||
request = self.factory.get('')
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.tag_page(request, tag.identifier)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'tag.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
request = self.factory.get('')
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.tag_page(request, tag.identifier)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_shelf_page(self):
|
|
||||||
''' there are so many views, this just makes sure it LOADS '''
|
|
||||||
shelf = self.local_user.shelf_set.first()
|
|
||||||
request = self.factory.get('')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = False
|
|
||||||
result = views.shelf_page(
|
|
||||||
request, self.local_user.username, shelf.identifier)
|
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
|
||||||
self.assertEqual(result.template_name, 'shelf.html')
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.shelf_page(
|
|
||||||
request, self.local_user.username, shelf.identifier)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
request = self.factory.get('/?page=1')
|
|
||||||
request.user = self.local_user
|
|
||||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
|
||||||
is_api.return_value = True
|
|
||||||
result = views.shelf_page(
|
|
||||||
request, self.local_user.username, shelf.identifier)
|
|
||||||
self.assertIsInstance(result, ActivitypubResponse)
|
|
||||||
self.assertEqual(result.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_bookwyrm_request(self):
|
def test_is_bookwyrm_request(self):
|
||||||
''' checks if a request came from a bookwyrm instance '''
|
''' checks if a request came from a bookwyrm instance '''
|
||||||
request = self.factory.get('', {'q': 'Test Book'})
|
request = self.factory.get('', {'q': 'Test Book'})
|
||||||
|
|
108
bookwyrm/tests/views/test_search.py
Normal file
108
bookwyrm/tests/views/test_search.py
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
''' test for app action functionality '''
|
||||||
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from bookwyrm import models, views
|
||||||
|
from bookwyrm.connectors import abstract_connector
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class ShelfViews(TestCase):
|
||||||
|
''' tag views'''
|
||||||
|
def setUp(self):
|
||||||
|
''' we need basic test data and mocks '''
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True, localname='mouse',
|
||||||
|
remote_id='https://example.com/users/mouse',
|
||||||
|
)
|
||||||
|
self.work = models.Work.objects.create(title='Test Work')
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Test Book',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
parent_work=self.work
|
||||||
|
)
|
||||||
|
models.Connector.objects.create(
|
||||||
|
identifier='self',
|
||||||
|
connector_file='self_connector',
|
||||||
|
local=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_json_response(self):
|
||||||
|
''' searches local data only and returns book data in json format '''
|
||||||
|
view = views.Search.as_view()
|
||||||
|
# we need a connector for this, sorry
|
||||||
|
request = self.factory.get('', {'q': 'Test Book'})
|
||||||
|
with patch('bookwyrm.views.search.is_api_request') as is_api:
|
||||||
|
is_api.return_value = True
|
||||||
|
response = view(request)
|
||||||
|
self.assertIsInstance(response, JsonResponse)
|
||||||
|
|
||||||
|
data = json.loads(response.content)
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
self.assertEqual(data[0]['title'], 'Test Book')
|
||||||
|
self.assertEqual(
|
||||||
|
data[0]['key'], 'https://%s/book/%d' % (DOMAIN, self.book.id))
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_html_response(self):
|
||||||
|
''' searches remote connectors '''
|
||||||
|
view = views.Search.as_view()
|
||||||
|
class TestConnector(abstract_connector.AbstractMinimalConnector):
|
||||||
|
''' nothing added here '''
|
||||||
|
def format_search_result(self, search_result):
|
||||||
|
pass
|
||||||
|
def get_or_create_book(self, remote_id):
|
||||||
|
pass
|
||||||
|
def parse_search_data(self, data):
|
||||||
|
pass
|
||||||
|
models.Connector.objects.create(
|
||||||
|
identifier='example.com',
|
||||||
|
connector_file='openlibrary',
|
||||||
|
base_url='https://example.com',
|
||||||
|
books_url='https://example.com/books',
|
||||||
|
covers_url='https://example.com/covers',
|
||||||
|
search_url='https://example.com/search?q=',
|
||||||
|
)
|
||||||
|
connector = TestConnector('example.com')
|
||||||
|
|
||||||
|
search_result = abstract_connector.SearchResult(
|
||||||
|
key='http://www.example.com/book/1',
|
||||||
|
title='Gideon the Ninth',
|
||||||
|
author='Tamsyn Muir',
|
||||||
|
year='2019',
|
||||||
|
connector=connector
|
||||||
|
)
|
||||||
|
|
||||||
|
request = self.factory.get('', {'q': 'Test Book'})
|
||||||
|
with patch('bookwyrm.views.search.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
with patch(
|
||||||
|
'bookwyrm.connectors.connector_manager.search') as manager:
|
||||||
|
manager.return_value = [search_result]
|
||||||
|
response = view(request)
|
||||||
|
self.assertIsInstance(response, TemplateResponse)
|
||||||
|
self.assertEqual(response.template_name, 'search_results.html')
|
||||||
|
self.assertEqual(
|
||||||
|
response.context_data['book_results'][0].title, 'Gideon the Ninth')
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_html_response_users(self):
|
||||||
|
''' searches remote connectors '''
|
||||||
|
view = views.Search.as_view()
|
||||||
|
request = self.factory.get('', {'q': 'mouse'})
|
||||||
|
with patch('bookwyrm.views.search.is_api_request') as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
with patch('bookwyrm.connectors.connector_manager.search'):
|
||||||
|
response = view(request)
|
||||||
|
self.assertIsInstance(response, TemplateResponse)
|
||||||
|
self.assertEqual(response.template_name, 'search_results.html')
|
||||||
|
self.assertEqual(
|
||||||
|
response.context_data['user_results'][0], self.local_user)
|
|
@ -3,7 +3,7 @@ from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
|
|
||||||
from bookwyrm import incoming, outgoing, settings, vviews, views, wellknown
|
from bookwyrm import incoming, outgoing, settings, views, wellknown
|
||||||
from bookwyrm import view_actions as actions
|
from bookwyrm import view_actions as actions
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
|
|
||||||
|
@ -49,11 +49,11 @@ urlpatterns = [
|
||||||
views.PasswordReset.as_view()),
|
views.PasswordReset.as_view()),
|
||||||
re_path(r'^change-password/?$', views.ChangePassword),
|
re_path(r'^change-password/?$', views.ChangePassword),
|
||||||
|
|
||||||
#invites
|
# invites
|
||||||
re_path(r'^invite/?$', views.ManageInvites.as_view()),
|
re_path(r'^invite/?$', views.ManageInvites.as_view()),
|
||||||
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.Invite.as_view()),
|
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.Invite.as_view()),
|
||||||
|
|
||||||
#landing pages
|
# landing pages
|
||||||
re_path(r'^about/?$', views.About.as_view()),
|
re_path(r'^about/?$', views.About.as_view()),
|
||||||
path('', views.Home.as_view()),
|
path('', views.Home.as_view()),
|
||||||
re_path(r'^(?P<tab>home|local|federated)/?$', views.Feed.as_view()),
|
re_path(r'^(?P<tab>home|local|federated)/?$', views.Feed.as_view()),
|
||||||
|
@ -61,6 +61,9 @@ urlpatterns = [
|
||||||
re_path(r'^notifications/?$', views.Notifications.as_view()),
|
re_path(r'^notifications/?$', views.Notifications.as_view()),
|
||||||
re_path(r'^direct-messages/?$', views.DirectMessage.as_view()),
|
re_path(r'^direct-messages/?$', views.DirectMessage.as_view()),
|
||||||
|
|
||||||
|
# search
|
||||||
|
re_path(r'^search/?$', views.Search.as_view()),
|
||||||
|
|
||||||
# imports
|
# imports
|
||||||
re_path(r'^import/?$', views.Import.as_view()),
|
re_path(r'^import/?$', views.Import.as_view()),
|
||||||
re_path(r'^import/(\d+)/?$', views.ImportStatus.as_view()),
|
re_path(r'^import/(\d+)/?$', views.ImportStatus.as_view()),
|
||||||
|
@ -116,8 +119,6 @@ urlpatterns = [
|
||||||
re_path(r'^shelve/?$', views.shelve),
|
re_path(r'^shelve/?$', views.shelve),
|
||||||
re_path(r'^unshelve/?$', views.unshelve),
|
re_path(r'^unshelve/?$', views.unshelve),
|
||||||
|
|
||||||
re_path(r'^search/?$', vviews.search),
|
|
||||||
|
|
||||||
re_path(r'^edit-readthrough/?$', actions.edit_readthrough),
|
re_path(r'^edit-readthrough/?$', actions.edit_readthrough),
|
||||||
re_path(r'^delete-readthrough/?$', actions.delete_readthrough),
|
re_path(r'^delete-readthrough/?$', actions.delete_readthrough),
|
||||||
re_path(r'^create-readthrough/?$', actions.create_readthrough),
|
re_path(r'^create-readthrough/?$', actions.create_readthrough),
|
||||||
|
|
|
@ -16,3 +16,4 @@ from .tag import Tag, AddTag, RemoveTag
|
||||||
from .shelf import Shelf
|
from .shelf import Shelf
|
||||||
from .shelf import user_shelves_page, create_shelf, delete_shelf
|
from .shelf import user_shelves_page, create_shelf, delete_shelf
|
||||||
from .shelf import shelve, unshelve
|
from .shelf import shelve, unshelve
|
||||||
|
from .search import Search
|
||||||
|
|
53
bookwyrm/views/search.py
Normal file
53
bookwyrm/views/search.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
''' search views'''
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
|
from django.db.models.functions import Greatest
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.connectors import connector_manager
|
||||||
|
from bookwyrm.utils import regex
|
||||||
|
from .helpers import is_api_request
|
||||||
|
from .helpers import handle_remote_webfinger
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable= no-self-use
|
||||||
|
class Search(View):
|
||||||
|
''' search users or books '''
|
||||||
|
def get(self, request):
|
||||||
|
''' that search bar up top '''
|
||||||
|
query = request.GET.get('q')
|
||||||
|
min_confidence = request.GET.get('min_confidence', 0.1)
|
||||||
|
|
||||||
|
if is_api_request(request):
|
||||||
|
# only return local book results via json so we don't cascade
|
||||||
|
book_results = connector_manager.local_search(
|
||||||
|
query, min_confidence=min_confidence)
|
||||||
|
return JsonResponse([r.json() for r in book_results], safe=False)
|
||||||
|
|
||||||
|
# use webfinger for mastodon style account@domain.com username
|
||||||
|
if re.match(r'\B%s' % regex.full_username, query):
|
||||||
|
handle_remote_webfinger(query)
|
||||||
|
|
||||||
|
# do a local user search
|
||||||
|
user_results = models.User.objects.annotate(
|
||||||
|
similarity=Greatest(
|
||||||
|
TrigramSimilarity('username', query),
|
||||||
|
TrigramSimilarity('localname', query),
|
||||||
|
)
|
||||||
|
).filter(
|
||||||
|
similarity__gt=0.5,
|
||||||
|
).order_by('-similarity')[:10]
|
||||||
|
|
||||||
|
book_results = connector_manager.search(
|
||||||
|
query, min_confidence=min_confidence)
|
||||||
|
data = {
|
||||||
|
'title': 'Search Results',
|
||||||
|
'book_results': book_results,
|
||||||
|
'user_results': user_results,
|
||||||
|
'query': query,
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'search_results.html', data)
|
|
@ -1,16 +1,5 @@
|
||||||
''' views for pages you can go to in the application '''
|
''' views for pages you can go to in the application '''
|
||||||
import re
|
|
||||||
|
|
||||||
from django.contrib.postgres.search import TrigramSimilarity
|
|
||||||
from django.db.models.functions import Greatest
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.views.decorators.http import require_GET
|
|
||||||
|
|
||||||
from bookwyrm import outgoing
|
|
||||||
from bookwyrm import models
|
|
||||||
from bookwyrm.connectors import connector_manager
|
|
||||||
from bookwyrm.utils import regex
|
|
||||||
|
|
||||||
|
|
||||||
def is_api_request(request):
|
def is_api_request(request):
|
||||||
|
@ -28,40 +17,3 @@ def not_found_page(request, _):
|
||||||
''' 404s '''
|
''' 404s '''
|
||||||
return TemplateResponse(
|
return TemplateResponse(
|
||||||
request, 'notfound.html', {'title': 'Not found'}, status=404)
|
request, 'notfound.html', {'title': 'Not found'}, status=404)
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
|
||||||
def search(request):
|
|
||||||
''' that search bar up top '''
|
|
||||||
query = request.GET.get('q')
|
|
||||||
min_confidence = request.GET.get('min_confidence', 0.1)
|
|
||||||
|
|
||||||
if is_api_request(request):
|
|
||||||
# only return local book results via json so we don't cause a cascade
|
|
||||||
book_results = connector_manager.local_search(
|
|
||||||
query, min_confidence=min_confidence)
|
|
||||||
return JsonResponse([r.json() for r in book_results], safe=False)
|
|
||||||
|
|
||||||
# use webfinger for mastodon style account@domain.com username
|
|
||||||
if re.match(r'\B%s' % regex.full_username, query):
|
|
||||||
outgoing.handle_remote_webfinger(query)
|
|
||||||
|
|
||||||
# do a local user search
|
|
||||||
user_results = models.User.objects.annotate(
|
|
||||||
similarity=Greatest(
|
|
||||||
TrigramSimilarity('username', query),
|
|
||||||
TrigramSimilarity('localname', query),
|
|
||||||
)
|
|
||||||
).filter(
|
|
||||||
similarity__gt=0.5,
|
|
||||||
).order_by('-similarity')[:10]
|
|
||||||
|
|
||||||
book_results = connector_manager.search(
|
|
||||||
query, min_confidence=min_confidence)
|
|
||||||
data = {
|
|
||||||
'title': 'Search Results',
|
|
||||||
'book_results': book_results,
|
|
||||||
'user_results': user_results,
|
|
||||||
'query': query,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, 'search_results.html', data)
|
|
||||||
|
|
Loading…
Reference in a new issue