proper comment search

This commit is contained in:
byte[] 2019-11-30 20:06:08 -05:00
parent 927cc55073
commit 404141883a
6 changed files with 263 additions and 103 deletions

View file

@ -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

View file

@ -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

View file

@ -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]
.block
.block__header.block__header--light
= pagination
- true ->
p
' No comments found!
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")

View file

@ -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

View file

@ -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"

View file

@ -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