├── .env.example ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── config ├── config.exs ├── dev.exs ├── docs.exs ├── prod.exs └── test.exs ├── docker-compose.yml ├── entrypoint.sh ├── lib ├── phoenix_pagination.ex └── phoenix_pagination │ ├── html.ex │ ├── json.ex │ └── paginator.ex ├── mix.exs ├── mix.lock ├── priv └── repo │ └── migrations │ └── 20160626174300_create_products.exs └── test ├── fixtures └── paginator_data.ex ├── json_test.exs ├── paginator_test.exs ├── phoenix_pagination_test.exs ├── support ├── product.ex └── repo.ex └── test_helper.exs /.env.example: -------------------------------------------------------------------------------- 1 | export DB_USER="postgres" 2 | export DB_PASSWORD="postgres" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /docs 5 | /doc 6 | *.tar 7 | erl_crash.dump 8 | *.ez 9 | .env 10 | .elixir_ls 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM elixir:1.15.4 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y postgresql-client inotify-tools 5 | 6 | RUN mkdir /app 7 | COPY . /app 8 | WORKDIR /app 9 | 10 | RUN mix local.hex --force && mix local.rebar --force 11 | 12 | 13 | CMD ["/app/entrypoint.sh"] 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2017 FunkyStudio (http://funky.studio) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phoenix.Pagination 2 | 3 | Simple pagination for Ecto and Phoenix using plaing EEx templates. It is based on a fork of [Kerosene](https://github.com/elixirdrops/kerosene), we think that pagination markup should be handled in templates, rather than with helper functions. So, here it comes. 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/packages/phoenix_pagination), can be installed as: 8 | 9 | Add phoenix_pagination to your list of dependencies in `mix.exs`: 10 | ```elixir 11 | def deps do 12 | [{:phoenix_pagination, "~> 0.6.0"}] 13 | end 14 | ``` 15 | 16 | Add Phoenix.Pagination to your `repo.ex`: 17 | ```elixir 18 | defmodule MyApp.Repo do 19 | use Ecto.Repo, otp_app: :testapp 20 | use Phoenix.Pagination, per_page: 15 21 | end 22 | ``` 23 | 24 | ## Usage 25 | Start paginating your queries 26 | ```elixir 27 | def index(conn, params) do 28 | {products, pagination} = Product 29 | |> Product.with_lowest_price 30 | |> Repo.paginate(params) 31 | 32 | render(conn, "index.html", products: products, pagination: pagination) 33 | end 34 | ``` 35 | 36 | Add view helpers to your view 37 | ```elixir 38 | defmodule MyAppWeb.ProductView do 39 | use MyAppWeb, :view 40 | import Phoenix.Pagination.HTML 41 | end 42 | ``` 43 | 44 | Create your pagination template in `lib/my_app_web/templates/pagination/pagination.html.eex` 45 | ```elixir 46 | <%= pagination @conn, @pagination, [current_class: "is-current"], fn p -> %> 47 | 56 | <% end %> 57 | ``` 58 | 59 | You can have as many pagination templates as you want (even with custom filenames), and render it where you need: 60 | ```elixir 61 | <%= render MyAppWeb.PaginationView, "pagination.html", conn: @conn, pagination: @pagination %> 62 | ``` 63 | 64 | Building APIs or SPAs? no problem Phoenix.Pagination has support for Json. 65 | 66 | ```elixir 67 | defmodule MyAppWeb.ProductView do 68 | use MyAppWeb, :view 69 | import Phoenix.Pagination.JSON 70 | 71 | def render("index.json", %{products: products, pagination: pagination, conn: conn}) do 72 | %{data: render_many(products, MyAppWeb.ProductView, "product.json"), 73 | pagination: paginate(conn, pagination)} 74 | end 75 | 76 | def render("product.json", %{product: product}) do 77 | %{id: product.id, 78 | name: product.name, 79 | description: product.description, 80 | price: product.price} 81 | end 82 | end 83 | ``` 84 | 85 | 86 | You can also send in options to paginate helper look at the docs for more details. 87 | 88 | ## Contributing 89 | 90 | Please do send pull requests and bug reports, positive feedback is always welcome. 91 | 92 | We would like to thank 93 | 94 | * elixirdrops ([@elixirdrops](https://github.com/elixirdrops)) 95 | * [Kerosene](https://github.com/elixirdrops/kerosene) 96 | 97 | ## License 98 | 99 | Please take a look at LICENSE.md 100 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | import_config "#{config_env()}.exs" 6 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /config/docs.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :phoenix_pagination, ecto_repos: [Phoenix.Pagination.Repo] 4 | 5 | config :phoenix_pagination, Phoenix.Pagination.Repo, 6 | adapter: Ecto.Adapters.Postgres, 7 | username: System.get_env("DB_USER"), 8 | password: System.get_env("DB_PASSWORD"), 9 | database: "phoenix_pagination_dev", 10 | hostname: "postgres", 11 | pool: Ecto.Adapters.SQL.Sandbox, 12 | port: "5432" 13 | 14 | # shut up only log errors 15 | config :logger, :console, 16 | level: :error 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Version of docker-compose 2 | version: "3.3" 3 | 4 | services: 5 | web: 6 | build: 7 | context: . 8 | environment: 9 | DB_USER: postgres 10 | DB_PASSWORD: postgres 11 | DB_NAME: phoenix_pagination_dev 12 | DB_HOST: postgres 13 | volumes: 14 | - .:/app 15 | ports: 16 | - "4001:4000" 17 | networks: 18 | - default 19 | depends_on: 20 | - postgres 21 | postgres: 22 | image: postgres:14.3 23 | environment: 24 | POSTGRES_USER: postgres 25 | POSTGRES_PASSWORD: postgres 26 | restart: always 27 | ports: 28 | - "5432:5432" 29 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while ! pg_isready -q -h $DB_HOST -p 5432 -U $DB_USER 4 | do 5 | echo "$(date) - waiting for database to start" 6 | sleep 2 7 | done 8 | 9 | if [[ -z `psql -Atqc "\\list $DB_NAME"` ]]; then 10 | echo "Database $DB_NAME does not exist. Creating..." 11 | createdb -E UTF8 $DB_NAME -l en_US.UTF-8 -T template0 12 | mix ecto.migrate 13 | mix run priv/repo/seeds.exs 14 | echo "Database $DB_NAME created." 15 | fi 16 | 17 | exec mix phx.server -------------------------------------------------------------------------------- /lib/phoenix_pagination.ex: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.Pagination do 2 | @moduledoc """ 3 | Pagination for Ecto and Phoenix. 4 | """ 5 | 6 | defstruct items: [], per_page: 0, max_page: 0, page: 0, total_pages: 0, total_count: 0, params: [] 7 | import Ecto.Query 8 | 9 | alias Phoenix.Pagination 10 | 11 | @per_page 10 12 | @max_page 1000 13 | @page 1 14 | 15 | defmacro __using__(opts \\ []) do 16 | quote do 17 | def paginate(query, params \\ %{}, options \\ []) do 18 | opts = Keyword.merge(unquote(opts), options) 19 | Pagination.paginate(__MODULE__, query, params, opts) 20 | end 21 | end 22 | end 23 | 24 | def paginate(repo, query, params, opts) do 25 | paginate(repo, query, merge_options(opts, params)) 26 | end 27 | 28 | def paginate(repo, query, opts) do 29 | per_page = get_per_page(opts) 30 | max_page = get_max_page(opts) 31 | total_count = get_total_count(opts[:total_count], repo, query) 32 | total_pages = get_total_pages(total_count, per_page) 33 | page = get_page(opts, total_pages) 34 | offset = get_offset(total_count, page, per_page) 35 | params = opts[:params] 36 | 37 | phoenix_pagination = %Phoenix.Pagination { 38 | per_page: per_page, 39 | page: page, 40 | total_pages: total_pages, 41 | total_count: total_count, 42 | max_page: max_page, 43 | params: params 44 | } 45 | 46 | {get_items(repo, query, per_page, offset), phoenix_pagination} 47 | end 48 | 49 | defp get_items(repo, query, nil, _), do: repo.all(query) 50 | defp get_items(repo, query, limit, offset) do 51 | query 52 | |> limit(^limit) 53 | |> offset(^offset) 54 | |> repo.all 55 | end 56 | 57 | defp get_offset(total_pages, page, per_page) do 58 | page = case page > total_pages do 59 | true -> total_pages 60 | _ -> page 61 | end 62 | 63 | case page > 0 do 64 | true -> (page - 1) * per_page 65 | _ -> page 66 | end 67 | end 68 | 69 | defp get_total_count(count, _repo, _query) when is_integer(count) and count >= 0, do: count 70 | defp get_total_count(_count, repo, query) do 71 | primary_key = get_primary_key(query) 72 | 73 | total_pages = 74 | query 75 | |> exclude(:preload) 76 | |> exclude(:order_by) 77 | |> exclude(:select) 78 | |> select([i], count(field(i, ^primary_key), :distinct)) 79 | |> repo.one 80 | total_pages || 0 81 | end 82 | 83 | def get_primary_key(query) do 84 | new_query = case is_map(query) do 85 | true -> 86 | {_, source} = query.from.source 87 | source 88 | _ -> query 89 | end 90 | 91 | new_query 92 | |> apply(:__schema__, [:primary_key]) 93 | |> hd 94 | end 95 | 96 | def get_total_pages(_, nil), do: 1 97 | def get_total_pages(count, per_page) do 98 | (count / per_page) |> Float.ceil |> trunc 99 | end 100 | 101 | def get_per_page(params) do 102 | case Keyword.get(params, :per_page) do 103 | nil -> @per_page 104 | per_page -> per_page |> to_integer() 105 | end 106 | end 107 | 108 | def get_max_page(params) do 109 | case Keyword.get(params, :max_page) do 110 | nil -> @max_page 111 | max_page -> max_page 112 | end 113 | end 114 | 115 | def get_page(params, total_pages) do 116 | page = params |> Keyword.get(:page, 1) |> to_integer() 117 | 118 | case page > params[:max_page] do 119 | true -> total_pages 120 | _ -> page 121 | end 122 | end 123 | 124 | defp merge_options(opts, params) do 125 | page = Map.get(params, "page", 1) 126 | per_page = Map.get(params, "per_page", opts[:per_page]) 127 | max_page = opts[:max_page] || Application.get_env(:phoenix_pagination, :max_page, @max_page) 128 | Keyword.merge(opts, [page: page, per_page: per_page, params: params, max_page: max_page]) 129 | end 130 | 131 | def to_integer(i) when is_integer(i), do: abs(i) 132 | def to_integer(i) when is_binary(i) do 133 | case Integer.parse(i) do 134 | :error -> @page 135 | {page, _} -> page 136 | end 137 | end 138 | def to_integer(_), do: @page 139 | end 140 | -------------------------------------------------------------------------------- /lib/phoenix_pagination/html.ex: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.Pagination.HTML do 2 | @moduledoc """ 3 | HTML helpers to render the pagination links in templates. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | alias Phoenix.Pagination.Paginator 9 | import Paginator, only: [build_options: 1] 10 | 11 | @buttons_labels ~w(first previous next last)a 12 | 13 | defmacro __using__(_opts \\ []) do 14 | quote do 15 | import Phoenix.Pagination.HTML 16 | end 17 | end 18 | 19 | def pagination(conn, paginator, opts \\ [], fun) do 20 | opts = build_options(opts) 21 | 22 | page_list = conn 23 | |> Paginator.paginate(paginator, opts) 24 | 25 | %{ 26 | options: opts, 27 | all_links: page_list, 28 | page_items: list_links(page_list) 29 | } |> fun.() 30 | end 31 | 32 | def pagination_link(pagination, name, opts \\ []) do 33 | options = pagination.options ++ opts 34 | |> build_options 35 | 36 | pagination.all_links 37 | |> Enum.filter(fn {pagenum, _, _, _} -> pagenum === name end) 38 | |> build_link(name, options) 39 | end 40 | 41 | defp build_link(button, name, opts) when length(button) == 0 do 42 | case opts[:force_show] do 43 | true -> link text_label(name, opts[:label]), to: "", class: opts[:class], disabled: "disabled" 44 | _ -> nil 45 | end 46 | end 47 | defp build_link(button, _name, opts) do 48 | [{name, _, url, current}] = button 49 | link text_label(name, opts[:label]), to: url, class: css_class(current, opts) 50 | end 51 | 52 | defp text_label(name, nil), do: to_string(name) 53 | defp text_label(_, label), do: label 54 | 55 | defp list_links(page_list) do 56 | page_list 57 | |> Enum.filter(fn {label, _, _, _} -> !Enum.member?(@buttons_labels, label) end) 58 | |> Enum.map(fn {label, _, url, current} -> {label, url, current} end) 59 | end 60 | 61 | defp css_class(true, opts), do: "#{opts[:class]} #{opts[:current_class]}" 62 | defp css_class(false, opts), do: opts[:class] 63 | end 64 | -------------------------------------------------------------------------------- /lib/phoenix_pagination/json.ex: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.Pagination.JSON do 2 | @moduledoc """ 3 | JSON helpers to render the pagination links in json format. 4 | import the `Phoenix.Pagination.JSON` in your view module. 5 | 6 | defmodule MyApp.ProductView do 7 | use MyApp.Web, :view 8 | import Phoenix.Pagination.JSON 9 | 10 | def render("index.json", %{conn: conn, products: products, phoenix_pagination: phoenix_pagination}) do 11 | %{data: render_many(products, MyApp.ProductView, "product.json"), 12 | pagination: paginate(conn, phoenix_pagination)} 13 | end 14 | end 15 | 16 | Where `phoenix_pagination` is a `%Phoenix.Pagination{}` struct returned from `Repo.paginate/2`. 17 | 18 | `paginate` helper takes keyword list of `options`. 19 | paginate(phoenix_pagination, window: 5, next_label: ">>", previous_label: "<<", first: true, last: true, first_label: "First", last_label: "Last") 20 | """ 21 | use Phoenix.HTML 22 | import Phoenix.Pagination.Paginator, only: [build_options: 1] 23 | alias Phoenix.Pagination.Paginator 24 | 25 | defmacro __using__(_opts \\ []) do 26 | quote do 27 | import Phoenix.Pagination.JSON 28 | end 29 | end 30 | 31 | def paginate(conn, paginator, opts \\ []) do 32 | opts = build_options(opts) 33 | 34 | conn 35 | |> Paginator.paginate(paginator, opts) 36 | |> render_page_list() 37 | end 38 | 39 | def render_page_list(page_list) do 40 | Enum.map(page_list, fn {link_label, page, url, current} -> 41 | %{label: "#{link_label}", url: url, page: page, current: current} 42 | end) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/phoenix_pagination/paginator.ex: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.Pagination.Paginator do 2 | @moduledoc """ 3 | Helpers to render the pagination links and more. 4 | """ 5 | 6 | use Phoenix.HTML 7 | @default [window: 3, range: true, current_class: "is-current"] 8 | 9 | alias Plug.Conn.Query 10 | 11 | @doc false 12 | def paginate(conn, paginator, opts \\ []) do 13 | page = paginator.page 14 | total_pages = paginator.total_pages 15 | params = build_params(paginator.params, opts[:params]) 16 | page 17 | |> previous_page 18 | |> first_page(page, opts[:window], opts[:first]) 19 | |> page_list(page, total_pages, opts[:window], opts[:range]) 20 | |> next_page(page, total_pages) 21 | |> last_page(page, total_pages, opts[:window], opts[:last]) 22 | |> Enum.map(fn {l, p} -> 23 | {l, p, build_url(conn, Map.put(params, "page", p)), page == p} 24 | end) 25 | end 26 | 27 | @doc """ 28 | Generates a page list based on current window 29 | """ 30 | def page_list(list, page, total, window, true) when is_integer(window) and window >= 1 do 31 | page_list = left(page, total, window)..right(page, total, window) 32 | |> Enum.map(fn n -> {n, n} end) 33 | 34 | list ++ page_list 35 | end 36 | def page_list(list, _page, _total, _window, _range), do: list 37 | 38 | def left(page, _total, window) when page - window <= 1, do: 1 39 | def left(page, _total, window), do: page - window 40 | 41 | def right(page, total, window) when page + window >= total, do: total 42 | def right(page, _total, window), do: page + window 43 | 44 | def previous_page(page) when page > 1 do 45 | [{:previous, page - 1}] 46 | end 47 | def previous_page(_page), do: [] 48 | 49 | def next_page(list, page, total) when page < total do 50 | list ++ [{:next, page + 1}] 51 | end 52 | def next_page(list, _page, _total), do: list 53 | 54 | def first_page(list, page, window, true) when page - window > 1 do 55 | [{:first, 1} | list] 56 | end 57 | def first_page(list, _page, _window, _included), do: list 58 | 59 | def last_page(list, page, total, window, true) when page + window < total do 60 | list ++ [{:last, total}] 61 | end 62 | def last_page(list, _page, _total, _window, _included), do: list 63 | 64 | def build_url(conn, nil), do: conn.request_path 65 | def build_url(conn, params) do 66 | p = params 67 | |> Map.delete("id") 68 | |> Enum.filter(fn {k, _v} -> 69 | key = case is_atom(k) do 70 | true -> Atom.to_string(k) 71 | _ -> k 72 | end 73 | !String.ends_with?(key, "_id") 74 | end) 75 | 76 | "#{conn.request_path}?#{build_query(p)}" 77 | end 78 | 79 | @doc """ 80 | Constructs a query param from a keyword list 81 | """ 82 | def build_query(params) do 83 | params |> Query.encode 84 | end 85 | 86 | def build_params(params, params2) do 87 | params |> Map.merge(params2) |> normalize_keys() 88 | end 89 | 90 | def normalize_keys(params) when is_map(params) do 91 | for {key, val} <- params, into: %{}, do: {to_string(key), val} 92 | end 93 | def normalize_keys(params), do: params 94 | 95 | def build_options(opts) do 96 | params = opts[:params] || %{} 97 | opts = Keyword.merge(opts, [params: params]) 98 | Keyword.merge(@default, opts) 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.Pagination.Mixfile do 2 | use Mix.Project 3 | @version "0.6.0" 4 | 5 | def project do 6 | [app: :phoenix_pagination, 7 | version: @version, 8 | elixir: "~> 1.14", 9 | elixirc_paths: path(Mix.env), 10 | package: package(), 11 | build_embedded: Mix.env == :prod, 12 | start_permanent: Mix.env == :prod, 13 | deps: deps(), 14 | aliases: aliases(), 15 | name: "phoenix_pagination", 16 | docs: [ 17 | main: "readme", 18 | extras: ["README.md"], 19 | source_ref: "v#{@version}"], 20 | source_url: "https://github.com/lorenzopagano/phoenix_pagination.git", 21 | homepage_url: "https://github.com/FunkyStudioHQ/phoenix_pagination", 22 | description: """ 23 | Simple pagination for Ecto and Phoenix using plaing EEx templates. 24 | """] 25 | end 26 | 27 | # Configuration for the OTP application 28 | # 29 | # Type "mix help compile.app" for more information 30 | def application do 31 | [applications: application(Mix.env)] 32 | end 33 | defp application(:test), do: [:postgrex, :ecto_sql, :logger] 34 | defp application(_), do: [:plug, :phoenix_html, :ecto, :ecto_sql, :logger] 35 | 36 | # Dependencies can be Hex packages: 37 | # 38 | # {:mydep, "~> 0.3.0"} 39 | # 40 | # Or git/path repositories: 41 | # 42 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 43 | # 44 | # Type "mix help deps" for more examples and options 45 | defp deps do 46 | [ 47 | {:phoenix_html, "~> 3.3"}, 48 | {:plug, "~> 1.14"}, 49 | {:ecto_sql, "~> 3.7"}, 50 | # Test dependencies 51 | {:postgrex, ">= 0.0.0", only: [:test]}, 52 | # {:credo, "~> 1.0.0", only: [:dev, :test], runtime: false}, 53 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 54 | # Docs dependencies 55 | {:earmark, "~> 1.4", only: :dev}, 56 | {:ex_doc, "~> 0.26", only: :dev, runtime: false}, 57 | # {:inch_ex, "~> 0.5", only: :dev} 58 | {:inch_ex, github: "rrrene/inch_ex", only: [:dev, :test]} 59 | ] 60 | end 61 | 62 | defp path(:test) do 63 | ["lib", "test/support", "test/fixtures"] 64 | end 65 | defp path(_), do: ["lib"] 66 | 67 | defp package do 68 | [ 69 | maintainers: ["FunkyStudio"], 70 | licenses: ["MIT"], 71 | links: %{ 72 | "Github" => "https://github.com/FunkyStudioHQ/phoenix_pagination.git", 73 | "FunkyStudio" => "http://funky.studio" 74 | }, 75 | files: ~w(lib test config) ++ ~w(CHANGELOG.md LICENSE.md mix.exs README.md) 76 | ] 77 | end 78 | 79 | def aliases do 80 | [test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]] 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, 4 | "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, 5 | "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, 6 | "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, 7 | "earmark": {:hex, :earmark, "1.4.25", "43fd256e37f671528727745f0fc760227e16f40dced303216f766eade06b7b10", [:mix], [{:earmark_parser, "~> 1.4.25", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "073c606ea4c3cc52920e8ae8ebe788c6af03d3e39782b25e5d14ceaa2f692998"}, 8 | "earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"}, 9 | "ecto": {:hex, :ecto, "3.8.3", "5e681d35bc2cbb46dcca1e2675837c7d666316e5ada14eca6c9c609b6232817c", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "af92dd7815967bcaea0daaaccf31c3b23165432b1c7a475d84144efbc703d105"}, 10 | "ecto_sql": {:hex, :ecto_sql, "3.8.2", "d7d44bc8d45ba9c85485952710c80408632a7336eb811b045e791718d11ddb5b", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7b9b03d64360d6cc05dc263500a43c11740b5fd4552244c27efad358e98c75b3"}, 11 | "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, 12 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 13 | "inch_ex": {:git, "https://github.com/rrrene/inch_ex.git", "d37c3cd41ceda869696499569547d6f9a416751c", []}, 14 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 15 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 16 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, 17 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 18 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 19 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 20 | "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"}, 21 | "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, 22 | "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, 23 | "postgrex": {:hex, :postgrex, "0.16.3", "fac79a81a9a234b11c44235a4494d8565303fa4b9147acf57e48978a074971db", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "aeaae1d2d1322da4e5fe90d241b0a564ce03a3add09d7270fb85362166194590"}, 24 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 25 | } 26 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160626174300_create_products.exs: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.Pagination.Repo.Migrations.CreateProduct do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:products) do 6 | add :name, :string 7 | add :price, :decimal 8 | 9 | timestamps() 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/fixtures/paginator_data.ex: -------------------------------------------------------------------------------- 1 | defmodule PaginatorData do 2 | def page_list() do 3 | [ 4 | {"First", 1, "/products?category=25&page=1", false}, 5 | {"<", 6, "/products?category=25&page=6", false}, 6 | {2, 2, "/products?category=25&page=2", false}, 7 | {3, 3, "/products?category=25&page=3", false}, 8 | {4, 4, "/products?category=25&page=4", false}, 9 | {5, 5, "/products?category=25&page=5", false}, 10 | {6, 6, "/products?category=25&page=6", false}, 11 | {7, 7, "/products?category=25&page=7", true}, 12 | {8, 8, "/products?category=25&page=8", false}, 13 | {9, 9, "/products?category=25&page=9", false}, 14 | {10, 10, "/products?category=25&page=10", false}, 15 | {11, 11, "/products?category=25&page=11", false}, 16 | {12, 12, "/products?category=25&page=12", false}, 17 | {">", 8, "/products?category=25&page=8", false}, 18 | {"Last", 16, "/products?category=25&page=16", false} 19 | ] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/json_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.Pagination.JSONTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "renders a list of links in json format" do 5 | expected = [ 6 | %{label: "First", url: "/products?category=25&page=1", page: 1, current: false}, 7 | %{label: "<", url: "/products?category=25&page=6", page: 6, current: false}, 8 | %{label: "2", url: "/products?category=25&page=2", page: 2, current: false}, 9 | %{label: "3", url: "/products?category=25&page=3", page: 3, current: false}, 10 | %{label: "4", url: "/products?category=25&page=4", page: 4, current: false}, 11 | %{label: "5", url: "/products?category=25&page=5", page: 5, current: false}, 12 | %{label: "6", url: "/products?category=25&page=6", page: 6, current: false}, 13 | %{label: "7", url: "/products?category=25&page=7", page: 7, current: true}, 14 | %{label: "8", url: "/products?category=25&page=8", page: 8, current: false}, 15 | %{label: "9", url: "/products?category=25&page=9", page: 9, current: false}, 16 | %{label: "10", url: "/products?category=25&page=10", page: 10, current: false}, 17 | %{label: "11", url: "/products?category=25&page=11", page: 11, current: false}, 18 | %{label: "12", url: "/products?category=25&page=12", page: 12, current: false}, 19 | %{label: ">", url: "/products?category=25&page=8", page: 8, current: false}, 20 | %{label: "Last", url: "/products?category=25&page=16", page: 16, current: false} 21 | ] 22 | 23 | data = PaginatorData.page_list 24 | output = Phoenix.Pagination.JSON.render_page_list(data) 25 | 26 | assert expected == output 27 | end 28 | 29 | end -------------------------------------------------------------------------------- /test/paginator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.Pagination.PaginatorTest do 2 | use ExUnit.Case, async: true 3 | import Phoenix.Pagination.Paginator 4 | 5 | test "next page only if there are more pages" do 6 | assert next_page([], 10, 10) == [] 7 | assert next_page([{:previous, 9}], 10, 10) == [{:previous, 9}] 8 | assert next_page([], 10, 12) == [{:next, 11}] 9 | end 10 | 11 | test "generate previous page unless first" do 12 | assert previous_page(0) == [] 13 | assert previous_page(10) == [{:previous, 9}] 14 | end 15 | 16 | test "generate first page" do 17 | assert first_page([], 5, 3, true) == [{:first, 1}] 18 | assert first_page([], 5, 3, false) == [] 19 | assert first_page([], 3, 3, true) == [] 20 | end 21 | 22 | test "generate last page" do 23 | assert last_page([], 5, 10, 3, true) == [{:last, 10}] 24 | assert last_page([], 5, 10, 3, false) == [] 25 | assert last_page([], 5, 10, 3, false) == [] 26 | end 27 | 28 | test "encode query params" do 29 | params = [query: "foo", page: 2, per_page: 10] 30 | expected = "query=foo&page=2&per_page=10" 31 | assert build_query(params) == expected 32 | end 33 | 34 | test "build full abs url with params" do 35 | params = %{query: "foo", page: 2, per_page: 10, foo: [1,2]} 36 | conn = %{request_path: "http://localhost:4000/products"} 37 | 38 | expected = "http://localhost:4000/products?foo[]=1&foo[]=2&page=2&per_page=10&query=foo" 39 | assert build_url(conn, params) == expected 40 | end 41 | 42 | test "build full abs url with invalid params" do 43 | params = nil 44 | conn = %{request_path: "http://localhost:4000/products"} 45 | expected = "http://localhost:4000/products" 46 | assert build_url(conn, params) == expected 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/phoenix_pagination_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.PaginationTest do 2 | use ExUnit.Case 3 | alias Phoenix.Pagination.Repo 4 | alias Phoenix.Pagination.Product 5 | 6 | setup do 7 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Phoenix.Pagination.Repo) 8 | end 9 | 10 | defp create_products do 11 | for _ <- 1..15 do 12 | %Product { name: "Product 1", price: 100.00 } 13 | |> Repo.insert! 14 | end 15 | end 16 | 17 | test "per_page option" do 18 | create_products() 19 | {_items, phoenix_pagination} = Product |> Repo.paginate(%{}, per_page: 5) 20 | assert phoenix_pagination.per_page == 5 21 | end 22 | 23 | test "per_page default option" do 24 | create_products() 25 | {items, phoenix_pagination} = Product |> Repo.paginate(%{}, per_page: nil) 26 | assert length(items) == 10 27 | assert phoenix_pagination.total_pages == 2 28 | assert phoenix_pagination.total_count == 15 29 | end 30 | 31 | test "total pages based on per_page" do 32 | create_products() 33 | {_items, phoenix_pagination} = Product |> Repo.paginate(%{}, per_page: 5) 34 | assert phoenix_pagination.total_pages == 3 35 | end 36 | 37 | test "default config" do 38 | create_products() 39 | {items, phoenix_pagination} = Product |> Repo.paginate(%{}) 40 | assert phoenix_pagination.total_pages == 2 41 | assert phoenix_pagination.page == 1 42 | assert length(items) == 10 43 | end 44 | 45 | test "total pages calculation" do 46 | row_count = 100 47 | per_page = 10 48 | total_pages = 10 49 | assert Phoenix.Pagination.get_total_pages(row_count, per_page) == total_pages 50 | end 51 | 52 | test "total_count option" do 53 | create_products() 54 | {_items, phoenix_pagination} = Product |> Repo.paginate(%{}, total_count: 3, per_page: 5) 55 | assert phoenix_pagination.total_count == 3 56 | assert phoenix_pagination.total_pages == 1 57 | end 58 | 59 | test "page offset constraint" do 60 | create_products() 61 | {_items, phoenix_pagination} = Product 62 | |> Repo.paginate(%{"page" => 100}, total_count: 3, per_page: 5, max_page: 10) 63 | 64 | assert phoenix_pagination.total_count == 3 65 | assert phoenix_pagination.total_pages == 1 66 | assert phoenix_pagination.page == 1 67 | end 68 | 69 | test "fallbacks to count query when provided total_count is nil" do 70 | create_products() 71 | {_items, phoenix_pagination} = Product |> Repo.paginate(%{}, total_count: nil, per_page: 5) 72 | assert phoenix_pagination.total_count == 15 73 | assert phoenix_pagination.total_pages == 3 74 | end 75 | 76 | test "to_integer returns number" do 77 | assert Phoenix.Pagination.to_integer(10) == 10 78 | assert Phoenix.Pagination.to_integer("10") == 10 79 | assert Phoenix.Pagination.to_integer(nil) == 1 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/support/product.ex: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.Pagination.Product do 2 | use Ecto.Schema 3 | 4 | schema "products" do 5 | field :name, :string 6 | field :price, :decimal 7 | 8 | timestamps() 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/support/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.Pagination.Repo do 2 | use Ecto.Repo, otp_app: :phoenix_pagination, adapter: Ecto.Adapters.Postgres 3 | use Phoenix.Pagination, otp_app: :phoenix_pagination, per_page: 10 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Phoenix.Pagination.Repo.start_link() 2 | ExUnit.start() 3 | 4 | Ecto.Adapters.SQL.Sandbox.mode(Phoenix.Pagination.Repo, :manual) 5 | --------------------------------------------------------------------------------