From 1b89198e4044ca93420736e4e8a72445aca8cc02 Mon Sep 17 00:00:00 2001 From: Deimos Date: Mon, 14 Oct 2019 15:25:35 -0600 Subject: [PATCH] Allow users to search their own posts This actually involves the necessary backend changes for full comment search, but I'm a little nervous about the privacy implications of that, and don't want to enable it for everyone out of nowhere. So for now, this just allows users to search *their own* comments or topics. These views and templates are starting to get real ugly, and probably need a major re-thinking soon. --- ...77_add_search_column_index_for_comments.py | 55 +++++++++++++++ tildes/prospector.yaml | 1 + tildes/scss/modules/_form.scss | 16 ++++- tildes/scss/modules/_sidebar.scss | 8 +-- .../sql/init/triggers/comments/comments.sql | 11 +++ tildes/tildes/models/comment/comment.py | 8 ++- tildes/tildes/models/comment/comment_query.py | 5 ++ tildes/tildes/request_methods.py | 2 + tildes/tildes/routes.py | 1 + tildes/tildes/templates/user.jinja2 | 29 ++++++-- tildes/tildes/templates/user_search.jinja2 | 10 +++ tildes/tildes/views/user.py | 68 +++++++++++++++++++ 12 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 tildes/alembic/versions/679090fd4977_add_search_column_index_for_comments.py create mode 100644 tildes/tildes/templates/user_search.jinja2 diff --git a/tildes/alembic/versions/679090fd4977_add_search_column_index_for_comments.py b/tildes/alembic/versions/679090fd4977_add_search_column_index_for_comments.py new file mode 100644 index 0000000..f78ad6e --- /dev/null +++ b/tildes/alembic/versions/679090fd4977_add_search_column_index_for_comments.py @@ -0,0 +1,55 @@ +"""Add search column/index for comments + +Revision ID: 679090fd4977 +Revises: 9fc0033a2b61 +Create Date: 2019-10-12 17:46:13.418316 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "679090fd4977" +down_revision = "9fc0033a2b61" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "comments", sa.Column("search_tsv", postgresql.TSVECTOR(), nullable=True) + ) + op.create_index( + "ix_comments_search_tsv_gin", + "comments", + ["search_tsv"], + unique=False, + postgresql_using="gin", + ) + + op.execute( + """ + CREATE TRIGGER comment_update_search_tsv_insert + BEFORE INSERT ON comments + FOR EACH ROW + EXECUTE PROCEDURE tsvector_update_trigger(search_tsv, 'pg_catalog.english', markdown); + + CREATE TRIGGER comment_update_search_tsv_update + BEFORE UPDATE ON comments + FOR EACH ROW + WHEN (OLD.markdown IS DISTINCT FROM NEW.markdown) + EXECUTE PROCEDURE tsvector_update_trigger(search_tsv, 'pg_catalog.english', markdown); + """ + ) + + # increase the timeout since updating search for all comments could take a while + op.execute("SET statement_timeout TO '10min'") + op.execute( + "UPDATE comments SET search_tsv = to_tsvector('pg_catalog.english', markdown)" + ) + + +def downgrade(): + op.drop_index("ix_comments_search_tsv_gin", table_name="comments") + op.drop_column("comments", "search_tsv") diff --git a/tildes/prospector.yaml b/tildes/prospector.yaml index 848c712..d1d7bed 100644 --- a/tildes/prospector.yaml +++ b/tildes/prospector.yaml @@ -39,5 +39,6 @@ pylint: - too-many-locals # almost never helpful - too-many-public-methods # almost never helpful - too-many-return-statements # almost never helpful + - too-many-statements # almost never helpful - ungrouped-imports # let isort handle this - unnecessary-pass # I prefer using pass, even when it's not technically necessary diff --git a/tildes/scss/modules/_form.scss b/tildes/scss/modules/_form.scss index 345b00b..f7f589e 100644 --- a/tildes/scss/modules/_form.scss +++ b/tildes/scss/modules/_form.scss @@ -107,8 +107,20 @@ textarea.form-input { display: block; } -.form-search .form-input { - margin-right: 0.4rem; +.form-search { + margin-top: 1rem; + margin-bottom: 1rem; + + .btn { + width: auto; + padding: 0 0.4rem; + font-weight: normal; + } + + .form-input { + max-width: 15rem; + margin-right: 0.4rem; + } } #new-topic { diff --git a/tildes/scss/modules/_sidebar.scss b/tildes/scss/modules/_sidebar.scss index 0bddd15..4f71cc0 100644 --- a/tildes/scss/modules/_sidebar.scss +++ b/tildes/scss/modules/_sidebar.scss @@ -19,12 +19,10 @@ } .form-search { - margin-bottom: 1rem; + margin-top: 0; - .btn { - width: auto; - padding: 0 0.4rem; - font-weight: normal; + .form-input { + max-width: unset; } } diff --git a/tildes/sql/init/triggers/comments/comments.sql b/tildes/sql/init/triggers/comments/comments.sql index 6f99e13..24b334e 100644 --- a/tildes/sql/init/triggers/comments/comments.sql +++ b/tildes/sql/init/triggers/comments/comments.sql @@ -19,3 +19,14 @@ CREATE TRIGGER delete_comment_set_deleted_time_update FOR EACH ROW WHEN (OLD.is_deleted IS DISTINCT FROM NEW.is_deleted) EXECUTE PROCEDURE set_comment_deleted_time(); + +CREATE TRIGGER comment_update_search_tsv_insert + BEFORE INSERT ON comments + FOR EACH ROW + EXECUTE PROCEDURE tsvector_update_trigger(search_tsv, 'pg_catalog.english', markdown); + +CREATE TRIGGER comment_update_search_tsv_update + BEFORE UPDATE ON comments + FOR EACH ROW + WHEN (OLD.markdown IS DISTINCT FROM NEW.markdown) + EXECUTE PROCEDURE tsvector_update_trigger(search_tsv, 'pg_catalog.english', markdown); diff --git a/tildes/tildes/models/comment/comment.py b/tildes/tildes/models/comment/comment.py index 9701bed..185c69b 100644 --- a/tildes/tildes/models/comment/comment.py +++ b/tildes/tildes/models/comment/comment.py @@ -8,7 +8,8 @@ from datetime import datetime, timedelta from typing import Any, Optional, Sequence, Tuple, TYPE_CHECKING, Union from pyramid.security import Allow, Authenticated, Deny, DENY_ALL, Everyone -from sqlalchemy import Boolean, Column, ForeignKey, Integer, Text, TIMESTAMP +from sqlalchemy import Boolean, Column, ForeignKey, Index, Integer, Text, TIMESTAMP +from sqlalchemy.dialects.postgresql import TSVECTOR from sqlalchemy.orm import deferred, relationship from sqlalchemy.sql.expression import text @@ -87,6 +88,7 @@ class Comment(DatabaseModel): rendered_html: str = Column(Text, nullable=False) excerpt: str = Column(Text, nullable=False, server_default="") num_votes: int = Column(Integer, nullable=False, server_default="0", index=True) + search_tsv: Any = deferred(Column(TSVECTOR)) user: User = relationship("User", lazy=False, innerjoin=True) topic: Topic = relationship("Topic", innerjoin=True) @@ -94,6 +96,10 @@ class Comment(DatabaseModel): "Comment", uselist=False, remote_side=[comment_id] ) + __table_args__ = ( + Index("ix_comments_search_tsv_gin", "search_tsv", postgresql_using="gin"), + ) + @hybrid_property def markdown(self) -> str: """Return the comment's markdown.""" diff --git a/tildes/tildes/models/comment/comment_query.py b/tildes/tildes/models/comment/comment_query.py index d68dff8..509c623 100644 --- a/tildes/tildes/models/comment/comment_query.py +++ b/tildes/tildes/models/comment/comment_query.py @@ -6,6 +6,7 @@ from typing import Any from pyramid.request import Request +from sqlalchemy import func from sqlalchemy.sql.expression import and_ from tildes.enums import CommentSortOption @@ -93,6 +94,10 @@ class CommentQuery(PaginatedQuery): return self + def search(self, query: str) -> "CommentQuery": + """Restrict the comments to ones that match a search query (generative).""" + return self.filter(Comment.search_tsv.op("@@")(func.plainto_tsquery(query))) + def only_bookmarked(self) -> "CommentQuery": """Restrict the comments to ones that the user has bookmarked (generative).""" self._only_bookmarked = True diff --git a/tildes/tildes/request_methods.py b/tildes/tildes/request_methods.py index 931afe8..ba57622 100644 --- a/tildes/tildes/request_methods.py +++ b/tildes/tildes/request_methods.py @@ -96,6 +96,7 @@ def current_listing_base_url( "home": ("order", "period", "per_page", "tag", "unfiltered"), "search": ("order", "period", "per_page", "q"), "user": ("order", "per_page", "type"), + "user_search": ("order", "per_page", "type", "q"), } try: @@ -130,6 +131,7 @@ def current_listing_normal_url( "notifications": ("per_page",), "search": ("order", "period", "per_page", "q"), "user": ("order", "per_page"), + "user_search": ("order", "per_page", "q"), } try: diff --git a/tildes/tildes/routes.py b/tildes/tildes/routes.py index fe79337..766b115 100644 --- a/tildes/tildes/routes.py +++ b/tildes/tildes/routes.py @@ -64,6 +64,7 @@ def includeme(config: Configurator) -> None: with config.route_prefix_context("/user/{username}"): config.add_route("new_message", "/new_message", factory=user_by_username) config.add_route("user_messages", "/messages", factory=user_by_username) + config.add_route("user_search", "/search", factory=user_by_username) config.add_route("notifications", "/notifications", factory=LoggedInFactory) with config.route_prefix_context("/notifications"): diff --git a/tildes/tildes/templates/user.jinja2 b/tildes/tildes/templates/user.jinja2 index 3e13ba8..65c0d9e 100644 --- a/tildes/tildes/templates/user.jinja2 +++ b/tildes/tildes/templates/user.jinja2 @@ -42,9 +42,13 @@ {% if request.has_permission("view_history", user) %}
-
  • - All posts -
  • + {# Don't show the "All posts" option in search results, since it can't be used #} + {% if search is not defined %} +
  • + All posts +
  • + {% endif %} +
  • Topics
  • @@ -56,6 +60,10 @@ {% if order_options %}
    + {% if search %} + + {% endif %} +
    + +
    + + +
    + + {% endif %} + {% if posts %} {% if request.has_permission("view_history", user) and posts.has_prev_page %}