Browse Source

Add extremely basic search

Quite a few aspects of this are very hackish (especially as related to
the templates and things that needed to be done to allow
topic_listing.jinja2 to be inherited from for this new one), but it's a
lot better than nothing.
merge-requests/32/head
Deimos 6 years ago
parent
commit
59799c95db
  1. 63
      tildes/alembic/versions/50c251c4a19c_add_search_column_index_for_topics.py
  2. 8
      tildes/scss/modules/_sidebar.scss
  3. 1
      tildes/scss/modules/_site-header.scss
  4. 15
      tildes/sql/init/triggers/topics/topics.sql
  5. 2
      tildes/tildes/__init__.py
  6. 10
      tildes/tildes/models/topic/topic.py
  7. 5
      tildes/tildes/models/topic/topic_query.py
  8. 2
      tildes/tildes/routes.py
  9. 7
      tildes/tildes/templates/home.jinja2
  10. 31
      tildes/tildes/templates/search.jinja2
  11. 22
      tildes/tildes/templates/topic_listing.jinja2
  12. 57
      tildes/tildes/views/topic.py

63
tildes/alembic/versions/50c251c4a19c_add_search_column_index_for_topics.py

@ -0,0 +1,63 @@
"""Add search column/index for topics
Revision ID: 50c251c4a19c
Revises: d33fb803a153
Create Date: 2018-08-20 19:18:04.129255
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "50c251c4a19c"
down_revision = "d33fb803a153"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"topics", sa.Column("search_tsv", postgresql.TSVECTOR(), nullable=True)
)
op.create_index(
"ix_topics_search_tsv_gin",
"topics",
["search_tsv"],
unique=False,
postgresql_using="gin",
)
op.execute(
"""
UPDATE topics
SET search_tsv = to_tsvector('pg_catalog.english', title)
|| to_tsvector('pg_catalog.english', COALESCE(markdown, ''));
"""
)
op.execute(
"""
CREATE TRIGGER topic_update_search_tsv_insert
BEFORE INSERT ON topics
FOR EACH ROW
EXECUTE PROCEDURE tsvector_update_trigger(search_tsv, 'pg_catalog.english', title, markdown);
CREATE TRIGGER topic_update_search_tsv_update
BEFORE UPDATE ON topics
FOR EACH ROW
WHEN (
(OLD.title IS DISTINCT FROM NEW.title)
OR (OLD.markdown IS DISTINCT FROM NEW.markdown)
)
EXECUTE PROCEDURE tsvector_update_trigger(search_tsv, 'pg_catalog.english', title, markdown);
"""
)
def downgrade():
op.drop_index("ix_topics_search_tsv_gin", table_name="topics")
op.drop_column("topics", "search_tsv")
op.execute("DROP TRIGGER topic_update_search_tsv_insert ON topics")
op.execute("DROP TRIGGER topic_update_search_tsv_update ON topics")

8
tildes/scss/modules/_sidebar.scss

@ -11,6 +11,14 @@
.sidebar-controls .btn {
width: auto;
}
.form-search {
margin-bottom: 1rem;
.btn {
font-weight: normal;
}
}
}
.sidebar-controls {

1
tildes/scss/modules/_site-header.scss

@ -12,6 +12,7 @@
}
.site-header-context {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

15
tildes/sql/init/triggers/topics/topics.sql

@ -12,3 +12,18 @@ CREATE TRIGGER delete_topic_set_deleted_time_update
FOR EACH ROW
WHEN (OLD.is_deleted = false AND NEW.is_deleted = true)
EXECUTE PROCEDURE set_topic_deleted_time();
CREATE TRIGGER topic_update_search_tsv_insert
BEFORE INSERT ON topics
FOR EACH ROW
EXECUTE PROCEDURE tsvector_update_trigger(search_tsv, 'pg_catalog.english', title, markdown);
CREATE TRIGGER topic_update_search_tsv_update
BEFORE UPDATE ON topics
FOR EACH ROW
WHEN (
(OLD.title IS DISTINCT FROM NEW.title)
OR (OLD.markdown IS DISTINCT FROM NEW.markdown)
)
EXECUTE PROCEDURE tsvector_update_trigger(search_tsv, 'pg_catalog.english', title, markdown);

2
tildes/tildes/__init__.py

@ -142,6 +142,7 @@ def current_listing_base_url(
base_vars_by_route: Dict[str, Tuple[str, ...]] = {
"group": ("order", "period", "per_page", "tag", "unfiltered"),
"home": ("order", "period", "per_page", "tag", "unfiltered"),
"search": ("order", "period", "per_page", "q"),
"user": ("per_page", "type"),
}
@ -175,6 +176,7 @@ def current_listing_normal_url(
normal_vars_by_route: Dict[str, Tuple[str, ...]] = {
"group": ("order", "period", "per_page"),
"home": ("order", "period", "per_page"),
"search": ("order", "period", "per_page", "q"),
"user": ("per_page",),
}

10
tildes/tildes/models/topic/topic.py

@ -14,7 +14,7 @@ from sqlalchemy import (
Text,
TIMESTAMP,
)
from sqlalchemy.dialects.postgresql import ENUM, JSONB
from sqlalchemy.dialects.postgresql import ENUM, JSONB, TSVECTOR
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import deferred, relationship
@ -111,12 +111,16 @@ class Topic(DatabaseModel):
)
is_official: bool = Column(Boolean, nullable=False, server_default="false")
is_locked: bool = Column(Boolean, nullable=False, server_default="false")
search_tsv: Any = deferred(Column(TSVECTOR))
user: User = relationship("User", lazy=False, innerjoin=True)
group: Group = relationship("Group", innerjoin=True)
# Create a GiST index on the tags column
__table_args__ = (Index("ix_topics_tags_gist", _tags, postgresql_using="gist"),)
# Create specialized indexes
__table_args__ = (
Index("ix_topics_tags_gist", _tags, postgresql_using="gist"),
Index("ix_topics_search_tsv_gin", "search_tsv", postgresql_using="gin"),
)
@hybrid_property
def markdown(self) -> Optional[str]:

5
tildes/tildes/models/topic/topic_query.py

@ -3,6 +3,7 @@
from typing import Any, Sequence
from pyramid.request import Request
from sqlalchemy import func
from sqlalchemy.sql.expression import and_, null
from sqlalchemy_utils import Ltree
@ -137,3 +138,7 @@ class TopicQuery(PaginatedQuery):
# pylint: disable=protected-access
return self.filter(Topic._tags.descendant_of(tag)) # type: ignore
def search(self, query: str) -> "TopicQuery":
"""Restrict the topics to ones that match a search query (generative)."""
return self.filter(Topic.search_tsv.op("@@")(func.plainto_tsquery(query)))

2
tildes/tildes/routes.py

@ -17,6 +17,8 @@ def includeme(config: Configurator) -> None:
"""Set up application routes."""
config.add_route("home", "/")
config.add_route("search", "/search")
config.add_route("groups", "/groups")
config.add_route("login", "/login")

7
tildes/tildes/templates/home.jinja2

@ -17,6 +17,13 @@
{% endblock %}
{% block sidebar %}
<form class="form-search" method="GET" action="/search">
<div class="input-group">
<input type="text" class="form-input input-sm" name="q" id="q">
<button class="btn btn-sm input-group-btn">Search</button>
</div>
</form>
<h2>Home</h2>
<p>The home page shows topics from groups that you're subscribed to.</p>
{% if request.user %}

31
tildes/tildes/templates/search.jinja2

@ -0,0 +1,31 @@
{% extends 'topic_listing.jinja2' %}
{% block title %}Search results for "{{ search }}"{% endblock %}
{% block header_context_link %}<span class="site-header-context">Search: "{{ search }}"</span>{% endblock %}
{% block sidebar %}
<form class="form-search" method="GET" action="/search">
<div class="input-group">
<input type="text" class="form-input input-sm" name="q" id="q" value="{{ search }}">
<button class="btn btn-sm input-group-btn">Search</button>
</div>
</form>
<h2>Search results</h2>
<p><a href="/">Back to home page</a></p>
{% if request.user and request.user.subscriptions %}
<div class="divider"></div>
<ul class="nav">
<li>Subscriptions</li>
<ul class="nav">
{% for subscription in request.user.subscriptions|sort(attribute='group') %}
<li class="nav-item"><a href="/~{{ subscription.group.path }}">~{{ subscription.group.path }}</a></li>
{% endfor %}
</ul>
</ul>
{% endif %}
{% endblock %}

22
tildes/tildes/templates/topic_listing.jinja2

@ -46,9 +46,14 @@
<form class="form-listing-options" method="get">
<input type="hidden" name="order" value="{{ order.name.lower() }}">
{% if tag %}
{% if tag is defined and tag %}
<input type="hidden" name="tag" value="{{ tag }}">
{% endif %}
{% if search is defined %}
<input type="hidden" name="q" value="{{ search }}">
{% endif %}
<div class="form-group">
<label for="period">from</label>
<select id="period" name="period" class="form-select" data-js-time-period-select>
@ -71,7 +76,7 @@
</div>
</form>
{% if not is_default_view %}
{% if is_default_view is defined and not is_default_view %}
<form
{% if is_single_group %}
data-ic-patch-to="{{ request.route_url(
@ -95,6 +100,7 @@
{% endif %}
</div>
{% if search is not defined %}
<div class="topic-listing-filter">
{% if tag %}
Showing only topics with the tag "{{ tag|replace('_', ' ') }}".
@ -107,6 +113,7 @@
<a href="{{ request.current_listing_normal_url({'unfiltered': 'true'}) }}">View unfiltered list</a>
{% endif %}
</div>
{% endif %}
{% if not topics %}
<div class="empty">
@ -124,7 +131,7 @@
{% if topics %}
<ol class="topic-listing"
{% if rank_start is not none %}
{% if rank_start is defined and rank_start is not none %}
start="{{ rank_start }}"
{% endif %}
>
@ -133,7 +140,7 @@
<li>
{# only display the rank on topics if the rank_start variable is set #}
{% if rank_start is not none %}
{% if rank_start is defined and rank_start is not none %}
{{ render_topic_for_listing(
topic,
show_group=topic.group != request.context,
@ -168,6 +175,13 @@
{% endblock %}
{% block sidebar %}
<form class="form-search" method="GET" action="/search">
<div class="input-group">
<input type="text" class="form-input input-sm" name="q" id="q">
<button class="btn btn-sm input-group-btn">Search</button>
</div>
</form>
<h3>~{{ group.path }}</h3>
{% if group.short_description %}

57
tildes/tildes/views/topic.py

@ -164,6 +164,63 @@ def get_group_topics(
}
@view_config(route_name="search", renderer="search.jinja2")
@use_kwargs(TopicListingSchema(only=("after", "before", "order", "per_page", "period")))
@use_kwargs({"search": String(load_from="q")})
def get_search(
request: Request,
order: Any,
period: Any,
after: str,
before: str,
per_page: int,
search: str,
) -> dict:
"""Get a list of search results."""
# pylint: disable=too-many-arguments
if order is missing:
order = TopicSortOption.NEW
if period is missing:
period = None
query = (
request.query(Topic)
.join_all_relationships()
.search(search)
.apply_sort_option(order)
)
# restrict the time period, if not set to "all time"
if period:
query = query.inside_time_period(period)
# apply before/after pagination restrictions if relevant
if before:
query = query.before_id36(before)
if after:
query = query.after_id36(after)
topics = query.get_page(per_page)
period_options = [SimpleHoursPeriod(hours) for hours in (1, 12, 24, 72)]
# add the current period to the bottom of the dropdown if it's not one of the
# "standard" ones
if period and period not in period_options:
period_options.append(period)
return {
"search": search,
"topics": topics,
"order": order,
"order_options": TopicSortOption,
"period": period,
"period_options": period_options,
}
@view_config(
route_name="new_topic", renderer="new_topic.jinja2", permission="post_topic"
)

Loading…
Cancel
Save