mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-04 23:36:32 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
4fc230ec8b
19 changed files with 3385 additions and 8828 deletions
4
.github/workflows/black.yml
vendored
4
.github/workflows/black.yml
vendored
|
@ -8,6 +8,4 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v2
|
||||||
- uses: psf/black@stable
|
- uses: psf/black@21.4b2
|
||||||
with:
|
|
||||||
args: ". --check -l 80 -S"
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load humanize %}
|
|
||||||
|
|
||||||
{% block title %}{% trans "Directory" %}{% endblock %}
|
{% block title %}{% trans "Directory" %}{% endblock %}
|
||||||
|
|
||||||
|
@ -41,59 +39,7 @@
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<div class="column is-one-third">
|
<div class="column is-one-third">
|
||||||
<div class="card is-stretchable">
|
{% include 'directory/user_card.html' %}
|
||||||
<div class="card-content">
|
|
||||||
<div class="media">
|
|
||||||
<a href="{{ user.local_path }}" class="media-left">
|
|
||||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
|
||||||
</a>
|
|
||||||
<div class="media-content">
|
|
||||||
<a href="{{ user.local_path }}" class="is-block mb-2">
|
|
||||||
<span class="title is-4 is-block">{{ user.display_name }}</span>
|
|
||||||
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
|
||||||
</a>
|
|
||||||
{% include 'snippets/follow_button.html' with user=user %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{% if user.summary %}
|
|
||||||
{{ user.summary | to_markdown | safe | truncatechars_html:40 }}
|
|
||||||
{% else %} {% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer class="card-footer">
|
|
||||||
{% if user != request.user %}
|
|
||||||
{% if user.mutuals %}
|
|
||||||
<div class="card-footer-item">
|
|
||||||
<div class="has-text-centered">
|
|
||||||
<p class="title is-6 mb-0">{{ user.mutuals }}</p>
|
|
||||||
<p class="help">{% blocktrans count counter=user.mutuals %}follower you follow{% plural %}followers you follow{% endblocktrans %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% elif user.shared_books %}
|
|
||||||
<div class="card-footer-item">
|
|
||||||
<div class="has-text-centered">
|
|
||||||
<p class="title is-6 mb-0">{{ user.shared_books }}</p>
|
|
||||||
<p class="help">{% blocktrans count counter=user.shared_books %}book on your shelves{% plural %}books on your shelves{% endblocktrans %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
<div class="card-footer-item">
|
|
||||||
<div class="has-text-centered">
|
|
||||||
<p class="title is-6 mb-0">{{ user.status_set.count|intword }}</p>
|
|
||||||
<p class="help">{% trans "posts" %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer-item">
|
|
||||||
<div class="has-text-centered">
|
|
||||||
<p class="title is-6 mb-0">{{ user.last_active_date|naturalday }}</p>
|
|
||||||
<p class="help">{% trans "last active" %}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
57
bookwyrm/templates/directory/user_card.html
Normal file
57
bookwyrm/templates/directory/user_card.html
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
<div class="card is-stretchable">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media">
|
||||||
|
<a href="{{ user.local_path }}" class="media-left">
|
||||||
|
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||||
|
</a>
|
||||||
|
<div class="media-content">
|
||||||
|
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||||
|
<span class="title is-4 is-block">{{ user.display_name }}</span>
|
||||||
|
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||||
|
</a>
|
||||||
|
{% include 'snippets/follow_button.html' with user=user %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% if user.summary %}
|
||||||
|
{{ user.summary | to_markdown | safe | truncatechars_html:40 }}
|
||||||
|
{% else %} {% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="card-footer">
|
||||||
|
{% if user != request.user %}
|
||||||
|
{% if user.mutuals %}
|
||||||
|
<div class="card-footer-item">
|
||||||
|
<div class="has-text-centered">
|
||||||
|
<p class="title is-6 mb-0">{{ user.mutuals }}</p>
|
||||||
|
<p class="help">{% blocktrans count counter=user.mutuals %}follower you follow{% plural %}followers you follow{% endblocktrans %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif user.shared_books %}
|
||||||
|
<div class="card-footer-item">
|
||||||
|
<div class="has-text-centered">
|
||||||
|
<p class="title is-6 mb-0">{{ user.shared_books }}</p>
|
||||||
|
<p class="help">{% blocktrans count counter=user.shared_books %}book on your shelves{% plural %}books on your shelves{% endblocktrans %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-footer-item">
|
||||||
|
<div class="has-text-centered">
|
||||||
|
<p class="title is-6 mb-0">{{ user.status_set.count|intword }}</p>
|
||||||
|
<p class="help">{% trans "posts" %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer-item">
|
||||||
|
<div class="has-text-centered">
|
||||||
|
<p class="title is-6 mb-0">{{ user.last_active_date|naturalday }}</p>
|
||||||
|
<p class="help">{% trans "last active" %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
78
bookwyrm/templates/search/book.html
Normal file
78
bookwyrm/templates/search/book.html
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
{% extends 'search/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
|
||||||
|
{% if results %}
|
||||||
|
{% with results|first as local_results %}
|
||||||
|
<ul class="block">
|
||||||
|
{% for result in local_results.results %}
|
||||||
|
<li class="pd-4">
|
||||||
|
{% include 'snippets/search_result_text.html' with result=result %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
{% for result_set in results|slice:"1:" %}
|
||||||
|
{% if result_set.results %}
|
||||||
|
<section class="box has-background-white-bis">
|
||||||
|
{% if not result_set.connector.local %}
|
||||||
|
<header class="columns is-mobile">
|
||||||
|
<div class="column">
|
||||||
|
<h3 class="title is-5">
|
||||||
|
Results from
|
||||||
|
<a href="{{ result_set.connector.base_url }}" target="_blank">{{ result_set.connector.name|default:result_set.connector.identifier }}</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
{% trans "Show" as button_text %}
|
||||||
|
{% include 'snippets/toggle/open_button.html' with text=button_text small=True controls_text="more-results-panel" controls_uid=result_set.connector.identifier class="is-small" icon="arrow-down" pressed=forloop.first %}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="box has-background-white is-shadowless{% if not forloop.first %} is-hidden{% endif %}" id="more-results-panel-{{ result_set.connector.identifier }}">
|
||||||
|
<div class="is-flex is-flex-direction-row-reverse">
|
||||||
|
<div>
|
||||||
|
{% trans "Close" as button_text %}
|
||||||
|
{% include 'snippets/toggle/toggle_button.html' with label=button_text class="delete" nonbutton=True controls_text="more-results-panel" controls_uid=result_set.connector.identifier pressed=forloop.first %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="is-flex-grow-1">
|
||||||
|
{% for result in result_set.results %}
|
||||||
|
<li class="mb-5">
|
||||||
|
{% include 'snippets/search_result_text.html' with result=result remote_result=True %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="block">
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
{% if not remote %}
|
||||||
|
<a href="{{ request.path }}?q={{ query }}&type=book&remote=true">
|
||||||
|
{% trans "Load results from other catalogues" %}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'create-book' %}">
|
||||||
|
{% trans "Manually add book" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'login' %}">
|
||||||
|
{% trans "Log in to import or add books." %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% endblock %}
|
70
bookwyrm/templates/search/layout.html
Normal file
70
bookwyrm/templates/search/layout.html
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Search" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="block">
|
||||||
|
<h1 class="title">
|
||||||
|
{% blocktrans %}Search{% endblocktrans %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="block" action="{% url 'search' %}" method="GET">
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control">
|
||||||
|
<input type="input" class="input" name="q" value="{{ query }}" aria-label="{% trans 'Search query' %}">
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select" aria-label="{% trans 'Search type' %}">
|
||||||
|
<select name="type">
|
||||||
|
<option value="book" {% if type == "book" %}selected{% endif %}>{% trans "Books" %}</option>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<option value="user" {% if type == "user" %}selected{% endif %}>{% trans "Users" %}</option>
|
||||||
|
{% endif %}
|
||||||
|
<option value="list" {% if type == "list" %}selected{% endif %}>{% trans "Lists" %}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-primary">
|
||||||
|
<span>Search</span>
|
||||||
|
<span class="icon icon-search" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if query %}
|
||||||
|
<nav class="tabs">
|
||||||
|
<ul>
|
||||||
|
<li{% if type == "book" %} class="is-active"{% endif %}>
|
||||||
|
<a href="{% url 'search' %}?q={{ query }}&type=book">{% trans "Books" %}</a>
|
||||||
|
</li>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<li{% if type == "user" %} class="is-active"{% endif %}>
|
||||||
|
<a href="{% url 'search' %}?q={{ query }}&type=user">{% trans "Users" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li{% if type == "list" %} class="is-active"{% endif %}>
|
||||||
|
<a href="{% url 'search' %}?q={{ query }}&type=list">{% trans "Lists" %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
{% if not results %}
|
||||||
|
<p>
|
||||||
|
<em>{% blocktrans %}No results found for "{{ query }}"{% endblocktrans %}</em>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% block panel %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% include 'snippets/pagination.html' with page=results path=request.path %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
7
bookwyrm/templates/search/list.html
Normal file
7
bookwyrm/templates/search/list.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends 'search/layout.html' %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
|
||||||
|
{% include 'lists/list_items.html' with lists=results %}
|
||||||
|
|
||||||
|
{% endblock %}
|
14
bookwyrm/templates/search/user.html
Normal file
14
bookwyrm/templates/search/user.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends 'search/layout.html' %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
{% for user in results %}
|
||||||
|
<div class="column is-one-third">
|
||||||
|
{% include 'directory/user_card.html' %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -1,133 +0,0 @@
|
||||||
{% extends 'layout.html' %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block title %}{% trans "Search Results" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% with book_results|first as local_results %}
|
|
||||||
<div class="block">
|
|
||||||
<h1 class="title">{% blocktrans %}Search Results for "{{ query }}"{% endblocktrans %}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="block columns">
|
|
||||||
<div class="column">
|
|
||||||
<h2 class="title is-4">{% trans "Matching Books" %}</h2>
|
|
||||||
<section class="block">
|
|
||||||
{% if not local_results.results %}
|
|
||||||
<p><em>{% blocktrans %}No books found for "{{ query }}"{% endblocktrans %}</em></p>
|
|
||||||
{% if not user.is_authenticated %}
|
|
||||||
<p>
|
|
||||||
<a href="{% url 'login' %}">{% trans "Log in to import or add books." %}</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<ul>
|
|
||||||
{% for result in local_results.results %}
|
|
||||||
<li class="pd-4">
|
|
||||||
{% include 'snippets/search_result_text.html' with result=result %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
{% if book_results|slice:":1" and local_results.results %}
|
|
||||||
<div class="block">
|
|
||||||
<h3 class="title is-6">
|
|
||||||
{% trans "Didn't find what you were looking for?" %}
|
|
||||||
</h3>
|
|
||||||
{% trans "Show results from other catalogues" as button_text %}
|
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text small=True controls_text="more-results" %}
|
|
||||||
|
|
||||||
{% if local_results.results %}
|
|
||||||
{% trans "Hide results from other catalogues" as button_text %}
|
|
||||||
{% include 'snippets/toggle/close_button.html' with text=button_text small=True controls_text="more-results" %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="{% if local_results.results %}is-hidden{% endif %}" id="more-results">
|
|
||||||
{% for result_set in book_results|slice:"1:" %}
|
|
||||||
{% if result_set.results %}
|
|
||||||
<section class="box has-background-white-bis">
|
|
||||||
{% if not result_set.connector.local %}
|
|
||||||
<header class="columns is-mobile">
|
|
||||||
<div class="column">
|
|
||||||
<h3 class="title is-5">
|
|
||||||
Results from
|
|
||||||
<a href="{{ result_set.connector.base_url }}" target="_blank">{{ result_set.connector.name|default:result_set.connector.identifier }}</a>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="column is-narrow">
|
|
||||||
{% trans "Show" as button_text %}
|
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text small=True controls_text="more-results-panel" controls_uid=result_set.connector.identifier class="is-small" icon="arrow-down" pressed=forloop.first %}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="box has-background-white is-shadowless{% if not forloop.first %} is-hidden{% endif %}" id="more-results-panel-{{ result_set.connector.identifier }}">
|
|
||||||
<div class="is-flex is-flex-direction-row-reverse">
|
|
||||||
<div>
|
|
||||||
{% trans "Close" as button_text %}
|
|
||||||
{% include 'snippets/toggle/toggle_button.html' with label=button_text class="delete" nonbutton=True controls_text="more-results-panel" controls_uid=result_set.connector.identifier pressed=forloop.first %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="is-flex-grow-1">
|
|
||||||
{% for result in result_set.results %}
|
|
||||||
<li class="mb-5">
|
|
||||||
{% include 'snippets/search_result_text.html' with result=result remote_result=True %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="block">
|
|
||||||
<a href="/create-book">Manually add book</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<section class="box">
|
|
||||||
<h2 class="title is-4">{% trans "Matching Users" %}</h2>
|
|
||||||
{% if not user_results %}
|
|
||||||
<p><em>{% blocktrans %}No users found for "{{ query }}"{% endblocktrans %}</em></p>
|
|
||||||
{% endif %}
|
|
||||||
<ul>
|
|
||||||
{% for result in user_results %}
|
|
||||||
<li class="block">
|
|
||||||
<a href="{{ result.local_path }}">
|
|
||||||
{% include 'snippets/avatar.html' with user=result %}
|
|
||||||
{{ result.display_name }}
|
|
||||||
</a> ({{ result.username }})
|
|
||||||
{% include 'snippets/follow_button.html' with user=result %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
<section class="box">
|
|
||||||
<h2 class="title is-4">{% trans "Lists" %}</h2>
|
|
||||||
{% if not list_results %}
|
|
||||||
<p><em>{% blocktrans %}No lists found for "{{ query }}"{% endblocktrans %}</em></p>
|
|
||||||
{% endif %}
|
|
||||||
{% for result in list_results %}
|
|
||||||
<div class="block">
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'list' result.id %}">{{ result.name }}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endwith %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<nav class="pagination" aria-label="pagination">
|
<nav class="pagination is-centered" aria-label="pagination">
|
||||||
<a
|
<a
|
||||||
class="pagination-previous {% if not page.has_previous %}is-disabled{% endif %}"
|
class="pagination-previous {% if not page.has_previous %}is-disabled{% endif %}"
|
||||||
{% if page.has_previous %}
|
{% if page.has_previous %}
|
||||||
|
@ -23,4 +23,18 @@
|
||||||
{% trans "Next" %}
|
{% trans "Next" %}
|
||||||
<span class="icon icon-arrow-right" aria-hidden="true"></span>
|
<span class="icon icon-arrow-right" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{% if page.has_other_pages and page_range %}
|
||||||
|
<ul class="pagination-list">
|
||||||
|
{% for num in page_range %}
|
||||||
|
{% if num == page.number %}
|
||||||
|
<li><a class="pagination-link is-current" aria-label="Page {{ num }}" aria-current="page">{{ num }}</a></li>
|
||||||
|
{% elif num == '…' %}
|
||||||
|
<li><span class="pagination-ellipsis">…</span></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a class="pagination-link" aria-label="Goto page {{ num }}" href="{{ path }}?{% for k, v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v }}&{% endif %}{% endfor %}page={{ num }}{{ anchor }}">{{ num }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import json
|
import json
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -12,7 +13,7 @@ from bookwyrm.connectors import abstract_connector
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
class ShelfViews(TestCase):
|
class Views(TestCase):
|
||||||
"""tag views"""
|
"""tag views"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -52,7 +53,7 @@ class ShelfViews(TestCase):
|
||||||
self.assertEqual(data[0]["title"], "Test Book")
|
self.assertEqual(data[0]["title"], "Test Book")
|
||||||
self.assertEqual(data[0]["key"], "https://%s/book/%d" % (DOMAIN, self.book.id))
|
self.assertEqual(data[0]["key"], "https://%s/book/%d" % (DOMAIN, self.book.id))
|
||||||
|
|
||||||
def test_search_html_response(self):
|
def test_search_books(self):
|
||||||
"""searches remote connectors"""
|
"""searches remote connectors"""
|
||||||
view = views.Search.as_view()
|
view = views.Search.as_view()
|
||||||
|
|
||||||
|
@ -92,7 +93,7 @@ class ShelfViews(TestCase):
|
||||||
connector=connector,
|
connector=connector,
|
||||||
)
|
)
|
||||||
|
|
||||||
request = self.factory.get("", {"q": "Test Book"})
|
request = self.factory.get("", {"q": "Test Book", "remote": True})
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
with patch("bookwyrm.views.search.is_api_request") as is_api:
|
with patch("bookwyrm.views.search.is_api_request") as is_api:
|
||||||
is_api.return_value = False
|
is_api.return_value = False
|
||||||
|
@ -101,19 +102,44 @@ class ShelfViews(TestCase):
|
||||||
response = view(request)
|
response = view(request)
|
||||||
self.assertIsInstance(response, TemplateResponse)
|
self.assertIsInstance(response, TemplateResponse)
|
||||||
response.render()
|
response.render()
|
||||||
self.assertEqual(
|
self.assertEqual(response.context_data["results"][0].title, "Gideon the Ninth")
|
||||||
response.context_data["book_results"][0].title, "Gideon the Ninth"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_search_html_response_users(self):
|
def test_search_users(self):
|
||||||
"""searches remote connectors"""
|
"""searches remote connectors"""
|
||||||
view = views.Search.as_view()
|
view = views.Search.as_view()
|
||||||
request = self.factory.get("", {"q": "mouse"})
|
request = self.factory.get("", {"q": "mouse", "type": "user"})
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
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)
|
response = view(request)
|
||||||
|
|
||||||
self.assertIsInstance(response, TemplateResponse)
|
self.assertIsInstance(response, TemplateResponse)
|
||||||
response.render()
|
response.render()
|
||||||
self.assertEqual(response.context_data["user_results"][0], self.local_user)
|
self.assertEqual(response.context_data["results"][0], self.local_user)
|
||||||
|
|
||||||
|
def test_search_users_logged_out(self):
|
||||||
|
"""searches remote connectors"""
|
||||||
|
view = views.Search.as_view()
|
||||||
|
request = self.factory.get("", {"q": "mouse", "type": "user"})
|
||||||
|
|
||||||
|
anonymous_user = AnonymousUser
|
||||||
|
anonymous_user.is_authenticated = False
|
||||||
|
request.user = anonymous_user
|
||||||
|
|
||||||
|
response = view(request)
|
||||||
|
|
||||||
|
response.render()
|
||||||
|
self.assertFalse("results" in response.context_data)
|
||||||
|
|
||||||
|
def test_search_lists(self):
|
||||||
|
"""searches remote connectors"""
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
||||||
|
booklist = models.List.objects.create(
|
||||||
|
user=self.local_user, name="test list"
|
||||||
|
)
|
||||||
|
view = views.Search.as_view()
|
||||||
|
request = self.factory.get("", {"q": "test", "type": "list"})
|
||||||
|
request.user = self.local_user
|
||||||
|
response = view(request)
|
||||||
|
|
||||||
|
self.assertIsInstance(response, TemplateResponse)
|
||||||
|
response.render()
|
||||||
|
self.assertEqual(response.context_data["results"][0], booklist)
|
||||||
|
|
|
@ -163,7 +163,7 @@ urlpatterns = [
|
||||||
name="direct-messages-user",
|
name="direct-messages-user",
|
||||||
),
|
),
|
||||||
# search
|
# search
|
||||||
re_path(r"^search/?$", views.Search.as_view()),
|
re_path(r"^search/?$", views.Search.as_view(), name="search"),
|
||||||
# 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()),
|
||||||
|
@ -272,7 +272,7 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
re_path(r"%s/edit/?$" % book_path, views.EditBook.as_view()),
|
re_path(r"%s/edit/?$" % book_path, views.EditBook.as_view()),
|
||||||
re_path(r"%s/confirm/?$" % book_path, views.ConfirmEditBook.as_view()),
|
re_path(r"%s/confirm/?$" % book_path, views.ConfirmEditBook.as_view()),
|
||||||
re_path(r"^create-book/?$", views.EditBook.as_view()),
|
re_path(r"^create-book/?$", views.EditBook.as_view(), name="create-book"),
|
||||||
re_path(r"^create-book/confirm?$", views.ConfirmEditBook.as_view()),
|
re_path(r"^create-book/confirm?$", views.ConfirmEditBook.as_view()),
|
||||||
re_path(r"%s/editions(.json)?/?$" % book_path, views.Editions.as_view()),
|
re_path(r"%s/editions(.json)?/?$" % book_path, views.Editions.as_view()),
|
||||||
re_path(
|
re_path(
|
||||||
|
|
|
@ -18,6 +18,7 @@ from django.views.decorators.http import require_POST
|
||||||
from bookwyrm import forms, models
|
from bookwyrm import forms, models
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
from bookwyrm.connectors import connector_manager
|
from bookwyrm.connectors import connector_manager
|
||||||
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
from .helpers import is_api_request, privacy_filter
|
from .helpers import is_api_request, privacy_filter
|
||||||
from .helpers import get_user_from_username
|
from .helpers import get_user_from_username
|
||||||
|
|
||||||
|
@ -133,7 +134,7 @@ class List(View):
|
||||||
.order_by(directional_sort_by)
|
.order_by(directional_sort_by)
|
||||||
)
|
)
|
||||||
|
|
||||||
paginated = Paginator(items, 12)
|
paginated = Paginator(items, PAGE_LENGTH)
|
||||||
|
|
||||||
if query and request.user.is_authenticated:
|
if query and request.user.is_authenticated:
|
||||||
# search for books
|
# search for books
|
||||||
|
@ -156,9 +157,13 @@ class List(View):
|
||||||
).order_by("-updated_date")
|
).order_by("-updated_date")
|
||||||
][: 5 - len(suggestions)]
|
][: 5 - len(suggestions)]
|
||||||
|
|
||||||
|
page = paginated.get_page(request.GET.get("page"))
|
||||||
data = {
|
data = {
|
||||||
"list": book_list,
|
"list": book_list,
|
||||||
"items": paginated.get_page(request.GET.get("page")),
|
"items": page,
|
||||||
|
"page_range": paginated.get_elided_page_range(
|
||||||
|
page.number, on_each_side=2, on_ends=1
|
||||||
|
),
|
||||||
"pending_count": book_list.listitem_set.filter(approved=False).count(),
|
"pending_count": book_list.listitem_set.filter(approved=False).count(),
|
||||||
"suggested_books": suggestions,
|
"suggested_books": suggestions,
|
||||||
"list_form": forms.ListForm(instance=book_list),
|
"list_form": forms.ListForm(instance=book_list),
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.contrib.postgres.search import TrigramSimilarity
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
|
from django.core.paginator import Paginator
|
||||||
from django.db.models.functions import Greatest
|
from django.db.models.functions import Greatest
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
|
@ -9,6 +10,7 @@ from django.views import View
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.connectors import connector_manager
|
from bookwyrm.connectors import connector_manager
|
||||||
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
from .helpers import is_api_request, privacy_filter
|
from .helpers import is_api_request, privacy_filter
|
||||||
from .helpers import handle_remote_webfinger
|
from .helpers import handle_remote_webfinger
|
||||||
|
@ -22,6 +24,10 @@ class Search(View):
|
||||||
"""that search bar up top"""
|
"""that search bar up top"""
|
||||||
query = request.GET.get("q")
|
query = request.GET.get("q")
|
||||||
min_confidence = request.GET.get("min_confidence", 0.1)
|
min_confidence = request.GET.get("min_confidence", 0.1)
|
||||||
|
search_type = request.GET.get("type")
|
||||||
|
search_remote = (
|
||||||
|
request.GET.get("remote", False) and request.user.is_authenticated
|
||||||
|
)
|
||||||
|
|
||||||
if is_api_request(request):
|
if is_api_request(request):
|
||||||
# only return local book results via json so we don't cascade
|
# only return local book results via json so we don't cascade
|
||||||
|
@ -30,16 +36,58 @@ class Search(View):
|
||||||
)
|
)
|
||||||
return JsonResponse([r.json() for r in book_results], safe=False)
|
return JsonResponse([r.json() for r in book_results], safe=False)
|
||||||
|
|
||||||
data = {"query": query or ""}
|
if not search_type:
|
||||||
|
search_type = "user" if "@" in query else "book"
|
||||||
|
|
||||||
# use webfinger for mastodon style account@domain.com username
|
endpoints = {
|
||||||
if query and re.match(regex.full_username, query):
|
"book": book_search,
|
||||||
|
"user": user_search,
|
||||||
|
"list": list_search,
|
||||||
|
}
|
||||||
|
if not search_type in endpoints:
|
||||||
|
search_type = "book"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"query": query or "",
|
||||||
|
"type": search_type,
|
||||||
|
"remote": search_remote,
|
||||||
|
}
|
||||||
|
if query:
|
||||||
|
results = endpoints[search_type](
|
||||||
|
query, request.user, min_confidence, search_remote
|
||||||
|
)
|
||||||
|
if results:
|
||||||
|
paginated = Paginator(results, PAGE_LENGTH).get_page(
|
||||||
|
request.GET.get("page")
|
||||||
|
)
|
||||||
|
data["results"] = paginated
|
||||||
|
|
||||||
|
return TemplateResponse(request, "search/{:s}.html".format(search_type), data)
|
||||||
|
|
||||||
|
|
||||||
|
def book_search(query, _, min_confidence, search_remote=False):
|
||||||
|
"""the real business is elsewhere"""
|
||||||
|
if search_remote:
|
||||||
|
return connector_manager.search(query, min_confidence=min_confidence)
|
||||||
|
results = connector_manager.local_search(query, min_confidence=min_confidence)
|
||||||
|
if not results:
|
||||||
|
return None
|
||||||
|
return [{"results": results}]
|
||||||
|
|
||||||
|
|
||||||
|
def user_search(query, viewer, *_):
|
||||||
|
"""cool kids members only user search"""
|
||||||
|
# logged out viewers can't search users
|
||||||
|
if not viewer.is_authenticated:
|
||||||
|
return models.User.objects.none()
|
||||||
|
|
||||||
|
# use webfinger for mastodon style account@domain.com username to load the user if
|
||||||
|
# they don't exist locally (handle_remote_webfinger will check the db)
|
||||||
|
if re.match(regex.full_username, query):
|
||||||
handle_remote_webfinger(query)
|
handle_remote_webfinger(query)
|
||||||
|
|
||||||
# do a user search
|
return (
|
||||||
if request.user.is_authenticated:
|
models.User.viewer_aware_objects(viewer)
|
||||||
data["user_results"] = (
|
|
||||||
models.User.viewer_aware_objects(request.user)
|
|
||||||
.annotate(
|
.annotate(
|
||||||
similarity=Greatest(
|
similarity=Greatest(
|
||||||
TrigramSimilarity("username", query),
|
TrigramSimilarity("username", query),
|
||||||
|
@ -52,10 +100,12 @@ class Search(View):
|
||||||
.order_by("-similarity")[:10]
|
.order_by("-similarity")[:10]
|
||||||
)
|
)
|
||||||
|
|
||||||
# any relevent lists?
|
|
||||||
data["list_results"] = (
|
def list_search(query, viewer, *_):
|
||||||
|
"""any relevent lists?"""
|
||||||
|
return (
|
||||||
privacy_filter(
|
privacy_filter(
|
||||||
request.user,
|
viewer,
|
||||||
models.List.objects,
|
models.List.objects,
|
||||||
privacy_levels=["public", "followers"],
|
privacy_levels=["public", "followers"],
|
||||||
)
|
)
|
||||||
|
@ -70,8 +120,3 @@ class Search(View):
|
||||||
)
|
)
|
||||||
.order_by("-similarity")[:10]
|
.order_by("-similarity")[:10]
|
||||||
)
|
)
|
||||||
|
|
||||||
data["book_results"] = connector_manager.search(
|
|
||||||
query, min_confidence=min_confidence
|
|
||||||
)
|
|
||||||
return TemplateResponse(request, "search_results.html", data)
|
|
||||||
|
|
|
@ -57,12 +57,16 @@ class Shelf(View):
|
||||||
PAGE_LENGTH,
|
PAGE_LENGTH,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
page = paginated.get_page(request.GET.get("page"))
|
||||||
data = {
|
data = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"is_self": is_self,
|
"is_self": is_self,
|
||||||
"shelves": shelves.all(),
|
"shelves": shelves.all(),
|
||||||
"shelf": shelf,
|
"shelf": shelf,
|
||||||
"books": paginated.get_page(request.GET.get("page")),
|
"books": page,
|
||||||
|
"page_range": paginated.get_elided_page_range(
|
||||||
|
page.number, on_each_side=2, on_ends=1
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return TemplateResponse(request, "user/shelf/shelf.html", data)
|
return TemplateResponse(request, "user/shelf/shelf.html", data)
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue