From 404141883ad11677a83b431f952c189a587743fd Mon Sep 17 00:00:00 2001 From: "byte[]" Date: Sat, 30 Nov 2019 20:06:08 -0500 Subject: [PATCH] proper comment search --- lib/philomena/comments/query.ex | 134 ++++++++++++++++ .../controllers/comment_controller.ex | 76 ++-------- .../templates/comment/index.html.slime | 143 +++++++++++++----- .../image/_image_container.html.slime | 3 +- .../templates/layout/_header.html.slime | 2 +- lib/philomena_web/views/image_view.ex | 8 +- 6 files changed, 263 insertions(+), 103 deletions(-) create mode 100644 lib/philomena/comments/query.ex diff --git a/lib/philomena/comments/query.ex b/lib/philomena/comments/query.ex new file mode 100644 index 00000000..dc282072 --- /dev/null +++ b/lib/philomena/comments/query.ex @@ -0,0 +1,134 @@ +defmodule Philomena.Comments.Query do + alias Search.Parser + + def user_id_transform(_ctx, data) do + case Integer.parse(data) do + {int, _rest} -> + { + :ok, + %{ + bool: %{ + must: [ + %{term: %{anonymous: false}}, + %{term: %{user_id: int}} + ] + } + } + } + + _err -> + {:error, "Unknown `user_id' value."} + end + end + + def author_transform(_ctx, data) do + { + :ok, + %{ + bool: %{ + must: [ + %{term: %{anonymous: false}}, + %{wildcard: %{author: data}} + ] + } + } + } + end + + def user_my_transform(%{user: %{id: id}}, "comments"), + do: {:ok, %{term: %{user_id: id}}} + + def user_my_transform(_ctx, _value), + do: {:error, "Unknown `my' value."} + + int_fields = ~W(id) + date_fields = ~W(created_at) + literal_fields = ~W(image_id) + ngram_fields = ~W(body) + custom_fields = ~W(author user_id) + default_field = "body" + transforms = %{ + "user_id" => &Philomena.Comments.Query.user_id_transform/2, + "author" => &Philomena.Comments.Query.author_transform/2 + } + aliases = %{ + "created_at" => "posted_at", + } + + user_custom = custom_fields ++ ~W(my) + user_transforms = Map.merge(transforms, %{ + "my" => &Philomena.Comments.Query.user_my_transform/2 + }) + + mod_literal_fields = literal_fields ++ ~W(fingerprint) + mod_ip_fields = ~W(ip) + mod_bool_fields = ~W(anonymous deleted) + mod_aliases = Map.merge(aliases, %{ + "deleted" => "hidden_from_users" + }) + + + @anonymous_parser Parser.parser( + int_fields: int_fields, + date_fields: date_fields, + literal_fields: literal_fields, + ngram_fields: ngram_fields, + custom_fields: custom_fields, + aliases: aliases, + default_field: default_field + ) + + @user_parser Parser.parser( + int_fields: int_fields, + date_fields: date_fields, + literal_fields: literal_fields, + ngram_fields: ngram_fields, + custom_fields: user_custom, + transforms: user_transforms, + aliases: aliases, + default_field: default_field + ) + + @moderator_parser Parser.parser( + int_fields: int_fields, + date_fields: date_fields, + literal_fields: mod_literal_fields, + ip_fields: mod_ip_fields, + ngram_fields: ngram_fields, + bool_fields: mod_bool_fields, + custom_fields: user_custom, + transforms: user_transforms, + aliases: mod_aliases, + default_field: default_field + ) + + def parse_anonymous(context, query_string) do + Parser.parse(@anonymous_parser, query_string, context) + end + + def parse_user(context, query_string) do + Parser.parse(@user_parser, query_string, context) + end + + def parse_moderator(context, query_string) do + Parser.parse(@moderator_parser, query_string, context) + end + + def compile(user, query_string) do + query_string = query_string || "" + + case user do + nil -> + parse_anonymous(%{user: nil}, query_string) + + %{role: role} when role in ~W(user assistant) -> + parse_user(%{user: user}, query_string) + + %{role: role} when role in ~W(moderator admin) -> + parse_moderator(%{user: user}, query_string) + + _ -> + raise ArgumentError, "Unknown user role." + end + end +end diff --git a/lib/philomena_web/controllers/comment_controller.ex b/lib/philomena_web/controllers/comment_controller.ex index 8703645e..1fa7f29c 100644 --- a/lib/philomena_web/controllers/comment_controller.ex +++ b/lib/philomena_web/controllers/comment_controller.ex @@ -1,21 +1,27 @@ defmodule PhilomenaWeb.CommentController do use PhilomenaWeb, :controller - alias Philomena.{Images.Image, Comments.Comment, Textile.Renderer} - alias Philomena.Repo + alias Philomena.{Comments.Query, Comments.Comment, Textile.Renderer} import Ecto.Query def index(conn, params) do + cq = params["cq"] || "created_at.gt:1 week ago" + + {:ok, query} = Query.compile(conn.assigns.current_user, cq) + comments = Comment.search_records( %{ query: %{ bool: %{ - must: parse_search(conn, params) ++ [%{term: %{hidden_from_users: false}}], - must_not: %{terms: %{image_tag_ids: conn.assigns.current_filter.hidden_tag_ids}} + must: query, + must_not: [ + %{terms: %{image_tag_ids: conn.assigns.current_filter.hidden_tag_ids}}, + %{term: %{hidden_from_users: true}} + ] } }, - sort: parse_sort(params) + sort: %{posted_at: :desc} }, conn.assigns.pagination, Comment |> preload([image: [:tags], user: [awards: :badge]]) @@ -26,64 +32,8 @@ defmodule PhilomenaWeb.CommentController do |> Renderer.render_collection() comments = - %{comments | entries: Enum.zip(comments.entries, rendered)} + %{comments | entries: Enum.zip(rendered, comments.entries)} - render(conn, "index.html", comments: comments, layout_class: "layout--wide") - end - - defp parse_search(conn, %{"comment" => comment_params}) do - parse_author(comment_params) ++ - parse_image_id(conn, comment_params) ++ - parse_body(comment_params) - end - defp parse_search(_conn, _params), do: [%{match_all: %{}}] - - defp parse_author(%{"author" => author}) when is_binary(author) and author not in [nil, ""] do - case String.contains?(author, ["*", "?"]) do - true -> - [ - %{wildcard: %{author: author}}, - %{term: %{anonymous: false}} - ] - - false -> - [ - %{term: %{author: author}}, - %{term: %{anonymous: false}} - ] - end - end - defp parse_author(_params), do: [] - - defp parse_image_id(conn, %{"image_id" => image_id}) when is_binary(image_id) and image_id not in [nil, ""] do - with {image_id, _rest} <- Integer.parse(image_id), - true <- valid_image?(conn.assigns.current_user, image_id) - do - [%{term: %{image_id: image_id}}] - else - _error -> - [] - end - end - defp parse_image_id(_conn, _params), do: [] - - defp parse_body(%{"body" => body}) when is_binary(body) and body not in [nil, ""], - do: [%{match: %{body: body}}] - defp parse_body(_params), do: [] - - defp parse_sort(%{"comment" => %{"sf" => sf, "sd" => sd}}) when sf in ["posted_at", "_score"] and sd in ["desc", "asc"] do - %{sf => sd} - end - defp parse_sort(_params) do - %{posted_at: :desc} - end - - defp valid_image?(user, image_id) do - image = - Image - |> where(id: ^image_id) - |> Repo.one() - - Canada.Can.can?(user, :show, image) + render(conn, "index.html", comments: comments) end end diff --git a/lib/philomena_web/templates/comment/index.html.slime b/lib/philomena_web/templates/comment/index.html.slime index 4fe8be5c..f12e3503 100644 --- a/lib/philomena_web/templates/comment/index.html.slime +++ b/lib/philomena_web/templates/comment/index.html.slime @@ -1,39 +1,114 @@ -elixir: - route = fn p -> Routes.comment_path(@conn, :index, p) end - pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @comments, route: route +h1 Comments + += form_for :comments, Routes.comment_path(@conn, :index), [method: "get", class: "hform", enforce_utf8: false], fn f -> + .field + = text_input f, :cq, name: :cq, value: @conn.params["cq"] || "created_at.gt:1 week ago", class: "input hform__text", placeholder: "Search comments", autocapitalize: "none" + = submit "Search", class: "hform__button button", data: [disable_with: false] + + .fieldlabel + ' For more information, see the + a href="/pages/search_syntax" search syntax documentation + ' . Search results are sorted by creation date. + +h2 Search Results + += cond do + - Enum.any?(@comments) -> + - route = fn p -> Routes.comment_path(@conn, :index, p) end + - pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @comments, route: route, params: [cq: @conn.params["cq"]], conn: @conn + + = for {body, comment} <- @comments, comment.image.hidden_from_users == false do + = render PhilomenaWeb.CommentView, "_comment_with_image.html", body: body, comment: comment, conn: @conn -.column-layout - .column-layout__left .block - .block__content - h3 Search Comments - - = form_for @conn, Routes.comment_path(@conn, :index), [as: :comment, method: "get", class: "hform"], fn f -> - .field = label f, :author, "Author" - .field = text_input f, :author, class: "input hform__text", placeholder: "Author (* is wildcard)" - - .field = label f, :image_id, "Image ID" - .field = number_input f, :image_id, class: "input hform__text", placeholder: "Image ID" - - .field = label f, :body, "Body" - .field = textarea f, :body, class: "input input--wide", placeholder: "Body" - - .field = label f, :sf, "Sort by" - .field - => select f, :sf, ["Creation Date": "posted_at", "Relevance": "_score"], class: "input" - => select f, :sd, ["Descending": "desc", "Ascending": "asc"], class: "input" - - .field - = submit "Search", class: "button button--state-primary" - - .column-layout__main - .block - .block__header + .block__header.block__header--light.flex = pagination + span.block__header__title + = render PhilomenaWeb.PaginationView, "_pagination_info.html", page: @comments, conn: @conn - = for {comment, body} <- @comments do - = render PhilomenaWeb.CommentView, "_comment_with_image.html", image: comment.image, comment: comment, body: body, conn: @conn + - assigns[:error] -> + p + ' Oops, there was an error evaluating your query: + pre = assigns[:error] + + - true -> + p + ' No comments found! - .block - .block__header.block__header--light - = pagination \ No newline at end of file +h3 Default search +p + ' If you do not specify a field to search over, the search engine will + ' search for comments with a body that is similar to the query's + em word stems + ' . For example, comments containing the words + code winged humanization + ' , + code wings + ' , and + code> spread wings + ' would all be found by a search for + code wing + ' , but + code> sewing + ' would not be. + +h3 Allowed fields +table.table + thead + tr + th Field Selector + th Type + th Description + th Example + tbody + tr + td + code author + td Literal + td Matches the author of this comment. Anonymous authors will never match this term. + td + code = link "author:Joey", to: Routes.comment_path(@conn, :index, cq: "author:Joey") + tr + td + code body + td Full Text + td Matches the body of this comment. This is the default field. + td + code = link "body:test", to: Routes.comment_path(@conn, :index, cq: "body:test") + tr + td + code created_at + td Date/Time Range + td Matches the creation time of this comment. + td + code = link "created_at:2015", to: Routes.comment_path(@conn, :index, cq: "created_at:2015") + tr + td + code id + td Numeric Range + td Matches the numeric surrogate key for this comment. + td + code = link "id:1000000", to: Routes.comment_path(@conn, :index, cq: "id:1000000") + tr + td + code image_id + td Literal + td Matches the numeric surrogate key for the image this comment belongs to. + td + code = link "image_id:1000000", to: Routes.comment_path(@conn, :index, cq: "image_id:1000000") + tr + td + code my + td Meta + td + code> my:comments + ' matches comments you have posted if you are signed in. + td + code = link "my:comments", to: Routes.comment_path(@conn, :index, cq: "my:comments") + tr + td + code user_id + td Literal + td Matches comments with the specified user_id. Anonymous users will never match this term. + td + code = link "user_id:211190", to: Routes.comment_path(@conn, :index, cq: "user_id:211190") \ No newline at end of file diff --git a/lib/philomena_web/templates/image/_image_container.html.slime b/lib/philomena_web/templates/image/_image_container.html.slime index f951c24d..bab2051a 100644 --- a/lib/philomena_web/templates/image/_image_container.html.slime +++ b/lib/philomena_web/templates/image/_image_container.html.slime @@ -1,4 +1,5 @@ - link = assigns[:link] || "/images/#{@image.id}" +- cookies = if assigns[:conn], do: @conn.cookies, else: %{} = image_container @image, @size, fn -> = cond do @@ -16,7 +17,7 @@ .media-box__overlay.js-spoiler-info-overlay a href=link - = case render_intent(@conn, @image, @size) do + = case render_intent(cookies, @image, @size) do - {:hidpi, small_url, medium_url, hover_text} -> picture img src=small_url srcset="#{small_url} 1x, #{medium_url} 2x" alt=hover_text diff --git a/lib/philomena_web/templates/layout/_header.html.slime b/lib/philomena_web/templates/layout/_header.html.slime index 2797ef73..5977e9f2 100644 --- a/lib/philomena_web/templates/layout/_header.html.slime +++ b/lib/philomena_web/templates/layout/_header.html.slime @@ -66,7 +66,7 @@ header.header a.header__link href='/search?q=my:uploads' i.fa.fa-fw.fa-upload> | Uploads - a.header__link href='/lists/my_comments' + a.header__link href='/comments?cq=my:comments' i.fa.fa-fw.fa-comments> | Comments a.header__link href="/posts/posted" diff --git a/lib/philomena_web/views/image_view.ex b/lib/philomena_web/views/image_view.ex index be6306d6..012cb2cb 100644 --- a/lib/philomena_web/views/image_view.ex +++ b/lib/philomena_web/views/image_view.ex @@ -7,16 +7,16 @@ defmodule PhilomenaWeb.ImageView do def show_vote_counts?(_user), do: true # this is a bit ridculous - def render_intent(_conn, %{thumbnails_generated: false}, _size), do: :not_rendered - def render_intent(conn, image, size) do + def render_intent(_cookies, %{thumbnails_generated: false}, _size), do: :not_rendered + def render_intent(cookies, image, size) do uris = thumb_urls(image, false) vid? = image.image_mime_type == "video/webm" gif? = image.image_mime_type == "image/gif" tags = Tag.display_order(image.tags) |> Enum.map_join(", ", & &1.name) alt = "Size: #{image.image_width}x#{image.image_height} | Tagged: #{tags}" - hidpi? = conn.cookies["hidpi"] == "true" - webm? = conn.cookies["webm"] == "true" + hidpi? = cookies["hidpi"] == "true" + webm? = cookies["webm"] == "true" use_gif? = vid? and not webm? and size in ~W(thumb thumb_small thumb_tiny)a cond do