diff --git a/lib/philomena/elasticsearch.ex b/lib/philomena/elasticsearch.ex index 097b414b..717efc28 100644 --- a/lib/philomena/elasticsearch.ex +++ b/lib/philomena/elasticsearch.ex @@ -9,6 +9,7 @@ defmodule Philomena.Elasticsearch do quote do alias Philomena.Repo import Ecto.Query, warn: false + require Logger def create_index! do Elastix.Index.create( @@ -81,7 +82,11 @@ defmodule Philomena.Elasticsearch do reindex(ecto_query, batch_size, ids) end - def search_results(elastic_query) do + def search_results(elastic_query, pagination_params \\ %{}) do + page_number = pagination_params[:page_number] || 1 + page_size = pagination_params[:page_size] || 25 + elastic_query = Map.merge(elastic_query, %{from: (page_number - 1) * page_size, size: page_size, _source: false}) + {:ok, %{body: results, status_code: 200}} = Elastix.Search.search( unquote(elastic_url), @@ -90,16 +95,32 @@ defmodule Philomena.Elasticsearch do elastic_query ) - results + time = results["took"] + count = results["hits"]["total"] + entries = results["hits"]["hits"] |> Enum.map(&String.to_integer(&1["_id"])) + + Logger.debug("[Elasticsearch] Query took #{time}ms") + + %Scrivener.Page{ + entries: entries, + page_number: page_number, + page_size: page_size, + total_entries: count, + total_pages: div(count + page_size - 1, page_size) + } end - def search_records(elastic_query, ecto_query \\ __MODULE__) do - results = search_results(elastic_query) + def search_records(elastic_query, pagination_params \\ %{}, ecto_query \\ __MODULE__) do + page = search_results(elastic_query, pagination_params) + ids = page.entries - ids = results["hits"]["hits"] |> Enum.map(&String.to_integer(&1["_id"])) - records = ecto_query |> where([m], m.id in ^ids) |> Repo.all() + records = + ecto_query + |> where([m], m.id in ^ids) + |> Repo.all() + |> Enum.sort_by(&Enum.find_index(ids, fn el -> el == &1.id end)) - records |> Enum.sort_by(&Enum.find_index(ids, fn el -> el == &1.id end)) + %{page | entries: records} end end end diff --git a/lib/philomena/repo.ex b/lib/philomena/repo.ex index 9161f9e0..cd5da3a5 100644 --- a/lib/philomena/repo.ex +++ b/lib/philomena/repo.ex @@ -2,4 +2,6 @@ defmodule Philomena.Repo do use Ecto.Repo, otp_app: :philomena, adapter: Ecto.Adapters.Postgres + + use Scrivener, page_size: 250 end diff --git a/lib/philomena_web/controllers/activity_controller.ex b/lib/philomena_web/controllers/activity_controller.ex index 9186b845..ba6f671e 100644 --- a/lib/philomena_web/controllers/activity_controller.ex +++ b/lib/philomena_web/controllers/activity_controller.ex @@ -19,9 +19,9 @@ defmodule PhilomenaWeb.ActivityController do must: image_query } }, - size: 25, sort: %{created_at: :desc} }, + %{page_number: 1, page_size: 25}, Image |> preload([:tags]) ) @@ -34,10 +34,9 @@ defmodule PhilomenaWeb.ActivityController do must: %{range: %{first_seen_at: %{gt: "now-3d"}}} } }, - size: 4, - from: :rand.uniform(26) - 1, sort: [%{score: :desc}, %{first_seen_at: :desc}] }, + %{page_number: :rand.uniform(6), page_size: 4}, Image |> preload([:tags]) ) @@ -55,9 +54,9 @@ defmodule PhilomenaWeb.ActivityController do ] } }, - size: 6, sort: %{posted_at: :desc} }, + %{page_number: 1, page_size: 6}, Comment |> preload([:user, :image]) ) @@ -72,9 +71,9 @@ defmodule PhilomenaWeb.ActivityController do must: watched_query } }, - size: 25, sort: %{created_at: :desc} }, + %{page_number: 1, page_size: 25}, Image |> preload([:tags]) ) end diff --git a/lib/philomena_web/controllers/image_controller.ex b/lib/philomena_web/controllers/image_controller.ex index be5e7803..4f8cbdff 100644 --- a/lib/philomena_web/controllers/image_controller.ex +++ b/lib/philomena_web/controllers/image_controller.ex @@ -16,6 +16,7 @@ defmodule PhilomenaWeb.ImageController do query: %{bool: %{must_not: [query, %{term: %{hidden_from_users: true}}]}}, sort: %{created_at: :desc} }, + conn.assigns.pagination, Image |> preload([:tags, :user]) ) diff --git a/lib/philomena_web/controllers/search_controller.ex b/lib/philomena_web/controllers/search_controller.ex index 5662ad4b..e01284b9 100644 --- a/lib/philomena_web/controllers/search_controller.ex +++ b/lib/philomena_web/controllers/search_controller.ex @@ -17,6 +17,7 @@ defmodule PhilomenaWeb.SearchController do query: %{bool: %{must: query, must_not: [filter, %{term: %{hidden_from_users: true}}]}}, sort: %{created_at: :desc} }, + conn.assigns.pagination, Image |> preload(:tags) ) diff --git a/lib/philomena_web/controllers/tag_controller.ex b/lib/philomena_web/controllers/tag_controller.ex index 355d9873..02687f88 100644 --- a/lib/philomena_web/controllers/tag_controller.ex +++ b/lib/philomena_web/controllers/tag_controller.ex @@ -14,6 +14,7 @@ defmodule PhilomenaWeb.TagController do size: 250, sort: [%{images: :desc}, %{name: :asc}] }, + %{conn.assigns.pagination | page_size: 250}, Tag ) @@ -36,6 +37,7 @@ defmodule PhilomenaWeb.TagController do }, sort: %{created_at: :desc} }, + conn.assigns.pagination, Image |> preload([:tags, :user]) ) diff --git a/lib/philomena_web/plugs/pagination.ex b/lib/philomena_web/plugs/pagination.ex new file mode 100644 index 00000000..6efe3b89 --- /dev/null +++ b/lib/philomena_web/plugs/pagination.ex @@ -0,0 +1,35 @@ +defmodule PhilomenaWeb.Plugs.Pagination do + import Plug.Conn + + # No options + def init([]), do: false + + # Assign pagination info + def call(conn, _opts) do + conn = conn |> fetch_query_params() + params = conn.params + + page_number = + case Integer.parse(params["page"] |> to_string()) do + {int, _rest} -> + int + _ -> + 1 + end + + page_number = page_number |> max(1) + + page_size = + case Integer.parse(params["per_page"] |> to_string()) do + {int, _rest} -> + int + _ -> + 25 + end + + page_size = page_size |> max(1) |> min(50) + + conn + |> assign(:pagination, %{page_number: page_number, page_size: page_size}) + end +end diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index fe052f7d..ff4cb4bd 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -9,6 +9,7 @@ defmodule PhilomenaWeb.Router do plug :protect_from_forgery plug :put_secure_browser_headers plug PhilomenaWeb.Plugs.ImageFilter + plug PhilomenaWeb.Plugs.Pagination end pipeline :api do @@ -26,6 +27,7 @@ defmodule PhilomenaWeb.Router do get "/", ActivityController, :index + resources "/activity", ActivityController, only: [:index] resources "/images", ImageController, only: [:index, :show] resources "/tags", TagController, only: [:index, :show] resources "/search", SearchController, only: [:index] diff --git a/lib/philomena_web/templates/activity/index.html.slime b/lib/philomena_web/templates/activity/index.html.slime index e38a6f97..326a2555 100644 --- a/lib/philomena_web/templates/activity/index.html.slime +++ b/lib/philomena_web/templates/activity/index.html.slime @@ -38,7 +38,7 @@ ' Most Commented-on Images .column-layout__main - = render PhilomenaWeb.ImageView, "index.html", images: @images, size: :thumb + = render PhilomenaWeb.ImageView, "index.html", conn: @conn, images: @images, size: :thumb = if !!@watched and @watched != [] do .block .block__header @@ -49,4 +49,4 @@ span.hide-mobile ' Browse Watched Images .block__content.js-resizable-media-container - = render PhilomenaWeb.ImageView, "index.html", images: @watched, size: :thumb_small + = render PhilomenaWeb.ImageView, "index.html", conn: @conn, images: @watched, size: :thumb_small diff --git a/lib/philomena_web/templates/image/index.html.slime b/lib/philomena_web/templates/image/index.html.slime index f98db93c..75b6802e 100644 --- a/lib/philomena_web/templates/image/index.html.slime +++ b/lib/philomena_web/templates/image/index.html.slime @@ -1,4 +1,25 @@ +- header = assigns[:header] || "" +- route = assigns[:route] || fn p -> Routes.image_path(@conn, :index, p) end +- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @images, route: route +- info = render PhilomenaWeb.PaginationView, "_pagination_info.html", page: @images + .block#imagelist-container + section.block__header.flex + h1.block__header__title.hide-mobile + => header + = pagination + .block__content.js-resizable-media-container = for image <- @images do - = render PhilomenaWeb.ImageView, "_image_box.html", image: image, size: assigns[:size] || :thumb \ No newline at end of file + = render PhilomenaWeb.ImageView, "_image_box.html", image: image, size: assigns[:size] || :thumb + + .block__header.block__header--light.flex + = pagination + + span.block__header__title + = info + .flex__right + a href="/settings/edit" title="Display Settings" + i.fa.fa-cog + span.hide-mobile.hide-limited-desktop<> + ' Display Settings \ No newline at end of file diff --git a/lib/philomena_web/templates/pagination/_pagination.html.slime b/lib/philomena_web/templates/pagination/_pagination.html.slime new file mode 100644 index 00000000..53741ae8 --- /dev/null +++ b/lib/philomena_web/templates/pagination/_pagination.html.slime @@ -0,0 +1,27 @@ +- params = assigns[:params] || [] + += if @page.total_pages > 1 do + nav.pagination + = if not first_page?(@page) do + = link("« First", to: first_page_path(@page, @route, params)) + = link("‹ Prev", to: prev_page_path(@page, @route, params)) + + = if left_gap?(@page) do + span.page.gap + ' … + + = for number <- left_page_numbers(@page) do + = link(number, to: page_path(@route, params, number)) + + span.page-current = @page.page_number + + = for number <- right_page_numbers(@page) do + = link(number, to: page_path(@route, params, number)) + + = if right_gap?(@page) do + span.page.gap + ' … + + = if not last_page?(@page) do + = link("Next ›", to: next_page_path(@page, @route, params)) + = link("Last »", to: last_page_path(@page, @route, params)) \ No newline at end of file diff --git a/lib/philomena_web/templates/pagination/_pagination_info.html.slime b/lib/philomena_web/templates/pagination/_pagination_info.html.slime new file mode 100644 index 00000000..f0731211 --- /dev/null +++ b/lib/philomena_web/templates/pagination/_pagination_info.html.slime @@ -0,0 +1,13 @@ +' Showing += if @page.total_entries == 1 do + ' result +- else + ' results +strong + => max(((@page.page_number - 1) * @page.page_size) - 1, 1) + ' - + => min(@page.page_number * @page.page_size, @page.total_entries) +' of +strong + => @page.total_entries +' total \ No newline at end of file diff --git a/lib/philomena_web/templates/tag/index.html.slime b/lib/philomena_web/templates/tag/index.html.slime index 4fb7b874..7c3ba2b6 100644 --- a/lib/philomena_web/templates/tag/index.html.slime +++ b/lib/philomena_web/templates/tag/index.html.slime @@ -1 +1,6 @@ -= render PhilomenaWeb.TagView, "_tag_list.html", tags: @tags \ No newline at end of file += render PhilomenaWeb.TagView, "_tag_list.html", tags: @tags + .block + .block__header.block__header--light.flex + = render PhilomenaWeb.PaginationView, "_pagination.html", page: @tags, route: fn p -> Routes.tag_path(@conn, :index, p) end + span.block__header__title + = render PhilomenaWeb.PaginationView, "_pagination_info.html", page: @tags \ No newline at end of file diff --git a/lib/philomena_web/views/pagination_view.ex b/lib/philomena_web/views/pagination_view.ex new file mode 100644 index 00000000..2a7c33a0 --- /dev/null +++ b/lib/philomena_web/views/pagination_view.ex @@ -0,0 +1,46 @@ +defmodule PhilomenaWeb.PaginationView do + use PhilomenaWeb, :view + + def first_page?(page) do + page.page_number == 1 + end + + def last_page?(page) do + page.page_number == page.total_pages + end + + def page_path(route, params, number) do + route.(Keyword.merge(params, page: number)) + end + + def first_page_path(_page, route, params), do: page_path(route, params, 1) + def prev_page_path(page, route, params), do: page_path(route, params, page.page_number - 1) + def next_page_path(page, route, params), do: page_path(route, params, page.page_number + 1) + def last_page_path(page, route, params), do: page_path(route, params, page.total_pages) + + def left_gap?(page) do + page.page_number >= 5 + end + + def left_page_numbers(page) do + number = page.page_number + min = 1 + max = page.total_pages + + (number - 5..number) + |> Enum.filter(& &1 >= min and &1 != number and &1 <= max) + end + + def right_gap?(page) do + page.total_pages - page.page_number >= 5 + end + + def right_page_numbers(page) do + number = page.page_number + min = 1 + max = page.total_pages + + (number .. number + 5) + |> Enum.filter(& &1 >= min and &1 != number and &1 <= max) + end +end diff --git a/mix.exs b/mix.exs index 097fc9e3..08d45af8 100644 --- a/mix.exs +++ b/mix.exs @@ -51,7 +51,8 @@ defmodule Philomena.MixProject do {:secure_compare, "~> 0.1.0"}, {:elastix, "~> 0.7.1"}, {:nimble_parsec, "~> 0.5.1"}, - {:canary, "~> 1.1.1"} + {:canary, "~> 1.1.1"}, + {:scrivener_ecto, "~> 2.0"} ] end diff --git a/mix.lock b/mix.lock index 475d0b7f..1cb5eff4 100644 --- a/mix.lock +++ b/mix.lock @@ -40,6 +40,8 @@ "pow": {:hex, :pow, "1.0.13", "5ca3e8d9fecca037bfb0ea3b8dde070cc319746498e844d59fc209d461b0d426", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3.0 or ~> 1.4.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 2.0.0 and <= 3.0.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, ">= 1.5.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, "retry": {:hex, :retry, "0.13.0", "bb9b2713f70f39337837852337ad280c77662574f4fb852a8386c269f3d734c4", [:mix], [], "hexpm"}, + "scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm"}, + "scrivener_ecto": {:hex, :scrivener_ecto, "2.2.0", "53d5f1ba28f35f17891cf526ee102f8f225b7024d1cdaf8984875467158c9c5e", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm"}, "secure_compare": {:hex, :secure_compare, "0.1.0", "01b3c93c8edb696e8a5b38397ed48e10958c8a5ec740606656445bcbec0aadb8", [:mix], [], "hexpm"}, "slime": {:hex, :slime, "1.2.0", "d46ede53c96b743dfdd23821268dc9b01f04ffea65d9d57c4e3d9200b162df02", [:mix], [{:neotoma, "~> 1.7", [hex: :neotoma, repo: "hexpm", optional: false]}], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"},