mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 21:47:59 +01:00
proper comment search
This commit is contained in:
parent
927cc55073
commit
404141883a
6 changed files with 263 additions and 103 deletions
134
lib/philomena/comments/query.ex
Normal file
134
lib/philomena/comments/query.ex
Normal 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
|
|
@ -1,21 +1,27 @@
|
||||||
defmodule PhilomenaWeb.CommentController do
|
defmodule PhilomenaWeb.CommentController do
|
||||||
use PhilomenaWeb, :controller
|
use PhilomenaWeb, :controller
|
||||||
|
|
||||||
alias Philomena.{Images.Image, Comments.Comment, Textile.Renderer}
|
alias Philomena.{Comments.Query, Comments.Comment, Textile.Renderer}
|
||||||
alias Philomena.Repo
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
def index(conn, params) do
|
def index(conn, params) do
|
||||||
|
cq = params["cq"] || "created_at.gt:1 week ago"
|
||||||
|
|
||||||
|
{:ok, query} = Query.compile(conn.assigns.current_user, cq)
|
||||||
|
|
||||||
comments =
|
comments =
|
||||||
Comment.search_records(
|
Comment.search_records(
|
||||||
%{
|
%{
|
||||||
query: %{
|
query: %{
|
||||||
bool: %{
|
bool: %{
|
||||||
must: parse_search(conn, params) ++ [%{term: %{hidden_from_users: false}}],
|
must: query,
|
||||||
must_not: %{terms: %{image_tag_ids: conn.assigns.current_filter.hidden_tag_ids}}
|
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,
|
conn.assigns.pagination,
|
||||||
Comment |> preload([image: [:tags], user: [awards: :badge]])
|
Comment |> preload([image: [:tags], user: [awards: :badge]])
|
||||||
|
@ -26,64 +32,8 @@ defmodule PhilomenaWeb.CommentController do
|
||||||
|> Renderer.render_collection()
|
|> Renderer.render_collection()
|
||||||
|
|
||||||
comments =
|
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")
|
render(conn, "index.html", comments: comments)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,39 +1,114 @@
|
||||||
elixir:
|
h1 Comments
|
||||||
route = fn p -> Routes.comment_path(@conn, :index, p) end
|
|
||||||
pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @comments, route: route
|
|
||||||
|
|
||||||
.column-layout
|
= form_for :comments, Routes.comment_path(@conn, :index), [method: "get", class: "hform", enforce_utf8: false], fn f ->
|
||||||
.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
|
.field
|
||||||
=> select f, :sf, ["Creation Date": "posted_at", "Relevance": "_score"], class: "input"
|
= 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"
|
||||||
=> select f, :sd, ["Descending": "desc", "Ascending": "asc"], class: "input"
|
= submit "Search", class: "hform__button button", data: [disable_with: false]
|
||||||
|
|
||||||
.field
|
.fieldlabel
|
||||||
= submit "Search", class: "button button--state-primary"
|
' For more information, see the
|
||||||
|
a href="/pages/search_syntax" search syntax documentation
|
||||||
|
' . Search results are sorted by creation date.
|
||||||
|
|
||||||
.column-layout__main
|
h2 Search Results
|
||||||
.block
|
|
||||||
.block__header
|
|
||||||
= pagination
|
|
||||||
|
|
||||||
= for {comment, body} <- @comments do
|
= cond do
|
||||||
= render PhilomenaWeb.CommentView, "_comment_with_image.html", image: comment.image, comment: comment, body: body, conn: @conn
|
- 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
|
||||||
|
|
||||||
.block
|
.block
|
||||||
.block__header.block__header--light
|
.block__header.block__header--light.flex
|
||||||
= pagination
|
= pagination
|
||||||
|
span.block__header__title
|
||||||
|
= render PhilomenaWeb.PaginationView, "_pagination_info.html", page: @comments, conn: @conn
|
||||||
|
|
||||||
|
- assigns[:error] ->
|
||||||
|
p
|
||||||
|
' Oops, there was an error evaluating your query:
|
||||||
|
pre = assigns[:error]
|
||||||
|
|
||||||
|
- 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")
|
|
@ -1,4 +1,5 @@
|
||||||
- link = assigns[:link] || "/images/#{@image.id}"
|
- link = assigns[:link] || "/images/#{@image.id}"
|
||||||
|
- cookies = if assigns[:conn], do: @conn.cookies, else: %{}
|
||||||
|
|
||||||
= image_container @image, @size, fn ->
|
= image_container @image, @size, fn ->
|
||||||
= cond do
|
= cond do
|
||||||
|
@ -16,7 +17,7 @@
|
||||||
|
|
||||||
.media-box__overlay.js-spoiler-info-overlay
|
.media-box__overlay.js-spoiler-info-overlay
|
||||||
a href=link
|
a href=link
|
||||||
= case render_intent(@conn, @image, @size) do
|
= case render_intent(cookies, @image, @size) do
|
||||||
- {:hidpi, small_url, medium_url, hover_text} ->
|
- {:hidpi, small_url, medium_url, hover_text} ->
|
||||||
picture
|
picture
|
||||||
img src=small_url srcset="#{small_url} 1x, #{medium_url} 2x" alt=hover_text
|
img src=small_url srcset="#{small_url} 1x, #{medium_url} 2x" alt=hover_text
|
||||||
|
|
|
@ -66,7 +66,7 @@ header.header
|
||||||
a.header__link href='/search?q=my:uploads'
|
a.header__link href='/search?q=my:uploads'
|
||||||
i.fa.fa-fw.fa-upload>
|
i.fa.fa-fw.fa-upload>
|
||||||
| Uploads
|
| Uploads
|
||||||
a.header__link href='/lists/my_comments'
|
a.header__link href='/comments?cq=my:comments'
|
||||||
i.fa.fa-fw.fa-comments>
|
i.fa.fa-fw.fa-comments>
|
||||||
| Comments
|
| Comments
|
||||||
a.header__link href="/posts/posted"
|
a.header__link href="/posts/posted"
|
||||||
|
|
|
@ -7,16 +7,16 @@ defmodule PhilomenaWeb.ImageView do
|
||||||
def show_vote_counts?(_user), do: true
|
def show_vote_counts?(_user), do: true
|
||||||
|
|
||||||
# this is a bit ridculous
|
# this is a bit ridculous
|
||||||
def render_intent(_conn, %{thumbnails_generated: false}, _size), do: :not_rendered
|
def render_intent(_cookies, %{thumbnails_generated: false}, _size), do: :not_rendered
|
||||||
def render_intent(conn, image, size) do
|
def render_intent(cookies, image, size) do
|
||||||
uris = thumb_urls(image, false)
|
uris = thumb_urls(image, false)
|
||||||
vid? = image.image_mime_type == "video/webm"
|
vid? = image.image_mime_type == "video/webm"
|
||||||
gif? = image.image_mime_type == "image/gif"
|
gif? = image.image_mime_type == "image/gif"
|
||||||
tags = Tag.display_order(image.tags) |> Enum.map_join(", ", & &1.name)
|
tags = Tag.display_order(image.tags) |> Enum.map_join(", ", & &1.name)
|
||||||
alt = "Size: #{image.image_width}x#{image.image_height} | Tagged: #{tags}"
|
alt = "Size: #{image.image_width}x#{image.image_height} | Tagged: #{tags}"
|
||||||
|
|
||||||
hidpi? = conn.cookies["hidpi"] == "true"
|
hidpi? = cookies["hidpi"] == "true"
|
||||||
webm? = conn.cookies["webm"] == "true"
|
webm? = cookies["webm"] == "true"
|
||||||
use_gif? = vid? and not webm? and size in ~W(thumb thumb_small thumb_tiny)a
|
use_gif? = vid? and not webm? and size in ~W(thumb thumb_small thumb_tiny)a
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
|
|
Loading…
Reference in a new issue