From 14d04ab0ca41c11afa3c6f6d04f73ab67a3113de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 27 Aug 2018 21:17:28 +0000 Subject: [PATCH 001/129] build(deps-dev): bump excoveralls from 0.9.2 to 0.10.0 Bumps [excoveralls](https://github.com/parroty/excoveralls) from 0.9.2 to 0.10.0. - [Release notes](https://github.com/parroty/excoveralls/releases) - [Changelog](https://github.com/parroty/excoveralls/blob/master/CHANGELOG.md) - [Commits](https://github.com/parroty/excoveralls/compare/v0.9.2...v0.10.0) Signed-off-by: dependabot[bot] --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index c52c95ad3..d636575b8 100644 --- a/mix.lock +++ b/mix.lock @@ -22,7 +22,7 @@ "ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [], [], "hexpm"}, "ex_unit_notifier": {:hex, :ex_unit_notifier, "0.1.4", "36a2dcab829f506e01bf17816590680dd1474407926d43e64c1263e627c364b8", [:mix], [], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.9.2", "299ea4903be7cb2959af0f919d258af116736ca8d507f86c12ef2184698e21a0", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.10.0", "a4508bdd408829f38e7b2519f234b7fd5c83846099cda348efcb5291b081200c", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "faker": {:hex, :faker, "0.10.0", "367c2ae47e7b4ac6410e1eaa880c07b5fe4194476f697d37ac1ce25f3058aae2", [:mix], [], "hexpm"}, "file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"}, "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm"}, From 64b769ce9f97e110389ac8d494cd9d469464ccd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 10 Sep 2018 21:29:30 +0000 Subject: [PATCH 002/129] build(deps): bump phoenix_ecto from 3.3.0 to 3.4.0 Bumps [phoenix_ecto](https://github.com/phoenixframework/phoenix_ecto) from 3.3.0 to 3.4.0. - [Release notes](https://github.com/phoenixframework/phoenix_ecto/releases) - [Changelog](https://github.com/phoenixframework/phoenix_ecto/blob/master/CHANGELOG.md) - [Commits](https://github.com/phoenixframework/phoenix_ecto/compare/v3.3.0...v3.4.0) Signed-off-by: dependabot[bot] --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 02fa208de..4983702de 100644 --- a/mix.exs +++ b/mix.exs @@ -53,7 +53,7 @@ defmodule MastaniServer.Mixfile do [ {:phoenix, "~> 1.3.2"}, {:phoenix_pubsub, "~> 1.1.0"}, - {:phoenix_ecto, "~> 3.3.0"}, + {:phoenix_ecto, "~> 3.4.0"}, {:ecto, "~> 2.2.9"}, {:postgrex, ">= 0.13.5"}, {:gettext, "~> 0.11"}, diff --git a/mix.lock b/mix.lock index c52c95ad3..b5f354f0c 100644 --- a/mix.lock +++ b/mix.lock @@ -39,7 +39,7 @@ "mix_test_watch": {:hex, :mix_test_watch, "0.8.0", "acf97da2abc66532e7dc1aa66a5d6c9fc4442d7992d5d7eb4faeaeb964c2580e", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "3.4.0", "91cd39427006fe4b5588d69f0941b9c3d3d8f5e6477c563a08379de7de2b0c58", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.0", "d55e25ff1ff8ea2f9964638366dfd6e361c52dedfd50019353598d11d4441d14", [:mix], [], "hexpm"}, "plug": {:hex, :plug, "1.6.2", "e06a7bd2bb6de5145da0dd950070110dce88045351224bd98e84edfdaaf5ffee", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, From ab0c67b33a24287b137af329e96d5791158df6bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 17 Sep 2018 21:17:26 +0000 Subject: [PATCH 003/129] build(deps): bump dataloader from 1.0.3 to 1.0.4 Bumps [dataloader](https://github.com/absinthe-graphql/dataloader) from 1.0.3 to 1.0.4. - [Release notes](https://github.com/absinthe-graphql/dataloader/releases) - [Changelog](https://github.com/absinthe-graphql/dataloader/blob/master/CHANGELOG.md) - [Commits](https://github.com/absinthe-graphql/dataloader/compare/v1.0.3...v1.0.4) Signed-off-by: dependabot[bot] --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index c52c95ad3..dcb885909 100644 --- a/mix.lock +++ b/mix.lock @@ -15,7 +15,7 @@ "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, "credo": {:hex, :credo, "0.10.0", "66234a95effaf9067edb19fc5d0cd5c6b461ad841baac42467afed96c78e5e9e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, - "dataloader": {:hex, :dataloader, "1.0.3", "3943b1b1ebe5ef59e88065cdbef9f1c9a9cb997fb90fed7ff4141b0b015eaa57", [:mix], [{:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, + "dataloader": {:hex, :dataloader, "1.0.4", "7c2345c53c9e5b61420013fc53c8463ba347a938b61f66677eb47d9c4a53ac5d", [:mix], [{:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, "dialyxir": {:hex, :dialyxir, "1.0.0-rc.3", "774306f84973fc3f1e2e8743eeaa5f5d29b117f3916e5de74c075c02f1b8ef55", [:mix], [], "hexpm"}, From 12875413f8d4e03976dc26a7cfe29acc2edcb8a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 17 Sep 2018 21:18:22 +0000 Subject: [PATCH 004/129] build(deps-dev): bump inch_ex from 1.0.0 to 1.0.1 Bumps [inch_ex](https://github.com/rrrene/inch_ex) from 1.0.0 to 1.0.1. - [Release notes](https://github.com/rrrene/inch_ex/releases) - [Changelog](https://github.com/rrrene/inch_ex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rrrene/inch_ex/compare/v1.0.0...v1.0.1) Signed-off-by: dependabot[bot] --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index c52c95ad3..3aee6b3b5 100644 --- a/mix.lock +++ b/mix.lock @@ -30,7 +30,7 @@ "guardian": {:hex, :guardian, "1.1.0", "36c1ea356a1bac02bc120c3f91f4f0259c5aa0ee92cee0efe8def5d7401f1921", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}, {:uuid, ">= 1.1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "inch_ex": {:hex, :inch_ex, "1.0.0", "18496a900ca4b7542a1ff1159e7f8be6c2012b74ca55ac70de5e805f14cdf939", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "inch_ex": {:hex, :inch_ex, "1.0.1", "1f0af1a83cec8e56f6fc91738a09c838e858db3d78ef5f2ec040fe4d5a62dabf", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, From 65d2ac2bdca260cd9287e59259a51aed61016aae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 24 Sep 2018 01:57:37 +0000 Subject: [PATCH 005/129] build(deps): bump guardian from 1.1.0 to 1.1.1 Bumps [guardian](https://github.com/ueberauth/guardian) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/ueberauth/guardian/releases) - [Changelog](https://github.com/ueberauth/guardian/blob/master/CHANGELOG.md) - [Commits](https://github.com/ueberauth/guardian/compare/v1.1.0...v1.1.1) Signed-off-by: dependabot[bot] --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index e0fbe0850..d8e64283a 100644 --- a/mix.lock +++ b/mix.lock @@ -27,7 +27,7 @@ "file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"}, "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, - "guardian": {:hex, :guardian, "1.1.0", "36c1ea356a1bac02bc120c3f91f4f0259c5aa0ee92cee0efe8def5d7401f1921", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}, {:uuid, ">= 1.1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm"}, + "guardian": {:hex, :guardian, "1.1.1", "be14c4007eaf05268251ae114030cb7237ed9a9631c260022f020164ff4ed733", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "inch_ex": {:hex, :inch_ex, "1.0.1", "1f0af1a83cec8e56f6fc91738a09c838e858db3d78ef5f2ec040fe4d5a62dabf", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, @@ -41,7 +41,7 @@ "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_ecto": {:hex, :phoenix_ecto, "3.4.0", "91cd39427006fe4b5588d69f0941b9c3d3d8f5e6477c563a08379de7de2b0c58", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.0", "d55e25ff1ff8ea2f9964638366dfd6e361c52dedfd50019353598d11d4441d14", [:mix], [], "hexpm"}, - "plug": {:hex, :plug, "1.6.2", "e06a7bd2bb6de5145da0dd950070110dce88045351224bd98e84edfdaaf5ffee", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, + "plug": {:hex, :plug, "1.6.3", "43088304337b9e8b8bd22a0383ca2f633519697e4c11889285538148f42cbc5e", [], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, From ed863771258ef86590338ddc5279c2747f6140a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 24 Sep 2018 01:58:08 +0000 Subject: [PATCH 006/129] build(deps-dev): bump mix_test_watch from 0.8.0 to 0.9.0 Bumps [mix_test_watch](https://github.com/lpil/mix-test.watch) from 0.8.0 to 0.9.0. - [Release notes](https://github.com/lpil/mix-test.watch/releases) - [Changelog](https://github.com/lpil/mix-test.watch/blob/master/CHANGELOG.md) - [Commits](https://github.com/lpil/mix-test.watch/compare/v0.8.0...v0.9.0) Signed-off-by: dependabot[bot] --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index e0fbe0850..4aba9a7a4 100644 --- a/mix.lock +++ b/mix.lock @@ -36,7 +36,7 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, - "mix_test_watch": {:hex, :mix_test_watch, "0.8.0", "acf97da2abc66532e7dc1aa66a5d6c9fc4442d7992d5d7eb4faeaeb964c2580e", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"}, + "mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_ecto": {:hex, :phoenix_ecto, "3.4.0", "91cd39427006fe4b5588d69f0941b9c3d3d8f5e6477c563a08379de7de2b0c58", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, From e2a22ca9c34d28c87684816b034ea1a8d214044b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 24 Sep 2018 01:58:18 +0000 Subject: [PATCH 007/129] build(deps-dev): bump credo from 0.10.0 to 0.10.1 Bumps [credo](https://github.com/rrrene/credo) from 0.10.0 to 0.10.1. - [Release notes](https://github.com/rrrene/credo/releases) - [Changelog](https://github.com/rrrene/credo/blob/master/CHANGELOG.md) - [Commits](https://github.com/rrrene/credo/compare/v0.10.0...v0.10.1) Signed-off-by: dependabot[bot] --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index e0fbe0850..e92a31223 100644 --- a/mix.lock +++ b/mix.lock @@ -14,7 +14,7 @@ "corsica": {:hex, :corsica, "1.1.2", "5ad8b9dcbeeda4762d78a57c0c8c2f88e1eef8741508517c98cb79e0db1f107d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, - "credo": {:hex, :credo, "0.10.0", "66234a95effaf9067edb19fc5d0cd5c6b461ad841baac42467afed96c78e5e9e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, + "credo": {:hex, :credo, "0.10.1", "e85efaf2dd7054399083ab2c6a5199f6cb1805de1a5b00d9e8c5f07033407b1f", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "dataloader": {:hex, :dataloader, "1.0.4", "7c2345c53c9e5b61420013fc53c8463ba347a938b61f66677eb47d9c4a53ac5d", [:mix], [{:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, From 302104bd293a6ef5db98152723a1342a6d230c5d Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 23 Sep 2018 16:37:36 +0800 Subject: [PATCH 008/129] build: production --- deploy/production/api_server.tar.gz | Bin 70796 -> 77863 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/deploy/production/api_server.tar.gz b/deploy/production/api_server.tar.gz index c424b7f50fe02917ef8821b7e73cd90caa1ddd33..31fa6aeadfe20c1926de6e8066608da4a2caafc4 100644 GIT binary patch literal 77863 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%0AbBTuMxO#kMgES1Q)%b&{Sx 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

Mn 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 From b0e7063e3a6b2626e25b44a2941bf845890e1e95 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 24 Sep 2018 11:54:27 +0800 Subject: [PATCH 009/129] ci(fix apollo schema check): schema check broken fix fix schema check broken caused by nested input_object --- .../resolvers/accounts_resolver.ex | 12 ++++- .../schema/account/account_misc.ex | 15 +++--- .../schema/account/account_mutations.ex | 3 +- .../mutation/accounts/account_test.exs | 48 +++++++++++++------ 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 304082e7b..1fdf8f88c 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -21,7 +21,17 @@ defmodule MastaniServerWeb.Resolvers.Accounts do User |> ORM.find(cur_user.id) end - def update_profile(_root, %{profile: profile}, %{context: %{cur_user: cur_user}}) do + def update_profile(_root, args, %{context: %{cur_user: cur_user}}) do + profile = + if Map.has_key?(args, :education_backgrounds), + do: Map.merge(args.profile, %{education_backgrounds: args.education_backgrounds}), + else: args.profile + + profile = + if Map.has_key?(args, :work_backgrounds), + do: Map.merge(profile, %{work_backgrounds: args.work_backgrounds}), + else: profile + Accounts.update_profile(%User{id: cur_user.id}, profile) end diff --git a/lib/mastani_server_web/schema/account/account_misc.ex b/lib/mastani_server_web/schema/account/account_misc.ex index 5d69fc9be..a78438aeb 100644 --- a/lib/mastani_server_web/schema/account/account_misc.ex +++ b/lib/mastani_server_web/schema/account/account_misc.ex @@ -32,16 +32,16 @@ defmodule MastaniServerWeb.Schema.Account.Misc do field(:public_gists, :integer) end - input_object :education_background do - field(:school, :string) - field(:major, :string) - end - - input_object :work_background do + input_object :work_background_input do field(:company, :string) field(:title, :string) end + input_object :edu_background_input do + field(:school, :string) + field(:major, :string) + end + input_object :user_profile_input do field(:nickname, :string) field(:bio, :string) @@ -50,9 +50,6 @@ defmodule MastaniServerWeb.Schema.Account.Misc do field(:email, :string) # social sscial_fields() - # backgrounds - field(:education_backgrounds, list_of(:education_background)) - field(:work_backgrounds, list_of(:work_background)) end # see: https://github.com/absinthe-graphql/absinthe/issues/206 diff --git a/lib/mastani_server_web/schema/account/account_mutations.ex b/lib/mastani_server_web/schema/account/account_mutations.ex index f7ce6839f..bdd081cbd 100644 --- a/lib/mastani_server_web/schema/account/account_mutations.ex +++ b/lib/mastani_server_web/schema/account/account_mutations.ex @@ -18,6 +18,8 @@ defmodule MastaniServerWeb.Schema.Account.Mutations do @desc "update user's profile" field :update_profile, :user do arg(:profile, non_null(:user_profile_input)) + arg(:work_backgrounds, list_of(:work_background_input)) + arg(:education_backgrounds, list_of(:edu_background_input)) middleware(M.Authorize, :login) resolve(&R.Accounts.update_profile/3) @@ -25,7 +27,6 @@ defmodule MastaniServerWeb.Schema.Account.Mutations do field :github_signin, :token_info do arg(:code, non_null(:string)) - # arg(:profile, non_null(:github_profile_input)) middleware(M.GithubUser) resolve(&R.Accounts.github_signin/3) diff --git a/test/mastani_server_web/mutation/accounts/account_test.exs b/test/mastani_server_web/mutation/accounts/account_test.exs index e476733d7..5a5b52223 100644 --- a/test/mastani_server_web/mutation/accounts/account_test.exs +++ b/test/mastani_server_web/mutation/accounts/account_test.exs @@ -15,8 +15,16 @@ defmodule MastaniServer.Test.Mutation.Account.Basic do describe "[account update]" do @update_query """ - mutation($profile: UserProfileInput!) { - updateProfile(profile: $profile) { + mutation( + $profile: UserProfileInput!, + $educationBackgrounds: [EduBackgroundInput], + $workBackgrounds: [WorkBackgroundInput] + ) { + updateProfile( + profile: $profile, + educationBackgrounds: $educationBackgrounds, + workBackgrounds: $workBackgrounds, + ) { id nickname education_backgrounds { @@ -44,23 +52,30 @@ defmodule MastaniServer.Test.Mutation.Account.Basic do assert updated["nickname"] == "new nickname" end + @tag :wip test "user can update it's own education_backgrounds", ~m(user)a do ownd_conn = simu_conn(:user, user) variables = %{ profile: %{ - nickname: "new nickname", - education_backgrounds: [ - %{ - school: "school", - major: "bad ass" - }, - %{ - school: "school2", - major: "bad ass2" - } - ] - } + nickname: "new nickname" + }, + educationBackgrounds: [ + %{ + school: "school", + major: "bad ass" + }, + %{ + school: "school2", + major: "bad ass2" + } + ], + workBackgrounds: [ + %{ + company: "cps", + title: "CTO" + } + ] } # assert ownd_conn |> mutation_get_error?(@update_query, variables) @@ -72,6 +87,11 @@ defmodule MastaniServer.Test.Mutation.Account.Basic do assert updated["education_backgrounds"] |> length == 2 assert updated["education_backgrounds"] |> Enum.any?(&(&1["school"] == "school")) assert updated["education_backgrounds"] |> Enum.any?(&(&1["major"] == "bad ass")) + + assert updated["work_backgrounds"] |> is_list + assert updated["work_backgrounds"] |> length == 1 + assert updated["work_backgrounds"] |> Enum.any?(&(&1["company"] == "cps")) + assert updated["work_backgrounds"] |> Enum.any?(&(&1["title"] == "CTO")) end test "user update education_backgrounds with invalid data fails", ~m(user)a do From dc46e656d78172e0560a7a7b71fd92ebb37ef9d7 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 24 Sep 2018 11:55:12 +0800 Subject: [PATCH 010/129] chore(deps): upgrade --- .travis.yml | 2 +- mix.exs | 6 +++--- mix.lock | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 06e42038b..086e1dee1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ node_js: - 8 sudo: false addons: - postgresql: '9.4' + postgresql: '10' before_script: - MIX_ENV=test mix deps.get - nvm install 8.10 && nvm use 8.10 diff --git a/mix.exs b/mix.exs index 4983702de..ea5a12839 100644 --- a/mix.exs +++ b/mix.exs @@ -51,15 +51,15 @@ defmodule MastaniServer.Mixfile do # Type `mix help deps` for examples and options. defp deps do [ - {:phoenix, "~> 1.3.2"}, + {:phoenix, "~> 1.3.4"}, {:phoenix_pubsub, "~> 1.1.0"}, {:phoenix_ecto, "~> 3.4.0"}, - {:ecto, "~> 2.2.9"}, + {:ecto, "~> 2.2.10"}, {:postgrex, ">= 0.13.5"}, {:gettext, "~> 0.11"}, {:cowboy, "~> 1.0"}, # GraphQl tool - {:absinthe, "~> 1.4.12"}, + {:absinthe, "~> 1.4.13"}, {:absinthe_ecto, "~> 0.1.3"}, # Plug support for Absinthe # {:absinthe_plug, "~> 1.4.4"}, diff --git a/mix.lock b/mix.lock index b2d3dea9f..dfbddeef5 100644 --- a/mix.lock +++ b/mix.lock @@ -41,7 +41,7 @@ "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_ecto": {:hex, :phoenix_ecto, "3.4.0", "91cd39427006fe4b5588d69f0941b9c3d3d8f5e6477c563a08379de7de2b0c58", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.0", "d55e25ff1ff8ea2f9964638366dfd6e361c52dedfd50019353598d11d4441d14", [:mix], [], "hexpm"}, - "plug": {:hex, :plug, "1.6.3", "43088304337b9e8b8bd22a0383ca2f633519697e4c11889285538148f42cbc5e", [], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, + "plug": {:hex, :plug, "1.6.3", "43088304337b9e8b8bd22a0383ca2f633519697e4c11889285538148f42cbc5e", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, From b443237ce63592f67fd56d8787debb51f9ebdff1 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 24 Sep 2018 12:03:40 +0800 Subject: [PATCH 011/129] build(postgresql): update to 10 --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 086e1dee1..7405ec8b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,13 @@ node_js: sudo: false addons: postgresql: '10' + apt: + packages: + - postgresql-10 + - postgresql-client-10 +env: + global: + - PGPORT=5433 before_script: - MIX_ENV=test mix deps.get - nvm install 8.10 && nvm use 8.10 From c0c6d4a2b4c98a237470ab69cab3315a262c73a2 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 24 Sep 2018 12:12:44 +0800 Subject: [PATCH 012/129] ci(postgresql): fallback to 9.4 --- .travis.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7405ec8b4..06e42038b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,14 +9,7 @@ node_js: - 8 sudo: false addons: - postgresql: '10' - apt: - packages: - - postgresql-10 - - postgresql-client-10 -env: - global: - - PGPORT=5433 + postgresql: '9.4' before_script: - MIX_ENV=test mix deps.get - nvm install 8.10 && nvm use 8.10 From 214a66ae9414069ed5ff13900413d897141880e7 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 24 Sep 2018 12:37:23 +0800 Subject: [PATCH 013/129] fix(profile test): fix work backgorund test --- .../mutation/accounts/account_test.exs | 35 +------------------ 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/test/mastani_server_web/mutation/accounts/account_test.exs b/test/mastani_server_web/mutation/accounts/account_test.exs index 5a5b52223..576a7b052 100644 --- a/test/mastani_server_web/mutation/accounts/account_test.exs +++ b/test/mastani_server_web/mutation/accounts/account_test.exs @@ -52,8 +52,7 @@ defmodule MastaniServer.Test.Mutation.Account.Basic do assert updated["nickname"] == "new nickname" end - @tag :wip - test "user can update it's own education_backgrounds", ~m(user)a do + test "user can update it's own backgrounds", ~m(user)a do ownd_conn = simu_conn(:user, user) variables = %{ @@ -78,8 +77,6 @@ defmodule MastaniServer.Test.Mutation.Account.Basic do ] } - # assert ownd_conn |> mutation_get_error?(@update_query, variables) - updated = ownd_conn |> mutation_result(@update_query, variables, "updateProfile") assert updated["nickname"] == "new nickname" @@ -115,36 +112,6 @@ defmodule MastaniServer.Test.Mutation.Account.Basic do assert ownd_conn |> mutation_get_error?(@update_query, variables) end - test "user can update it's own work backgrounds", ~m(user)a do - ownd_conn = simu_conn(:user, user) - - variables = %{ - profile: %{ - nickname: "new nickname", - work_backgrounds: [ - %{ - company: "company", - title: "bad ass" - }, - %{ - company: "company 2", - title: "bad ass2" - } - ] - } - } - - # assert ownd_conn |> mutation_get_error?(@update_query, variables) - - updated = ownd_conn |> mutation_result(@update_query, variables, "updateProfile") - assert updated["nickname"] == "new nickname" - - assert updated["work_backgrounds"] |> is_list - assert updated["work_backgrounds"] |> length == 2 - assert updated["work_backgrounds"] |> Enum.any?(&(&1["company"] == "company")) - assert updated["work_backgrounds"] |> Enum.any?(&(&1["title"] == "bad ass")) - end - test "user update work backgrounds with invalid data fails", ~m(user)a do ownd_conn = simu_conn(:user, user) From f8f86aab08082d3e67e2aa15ac7bf3be7f2cb342 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 24 Sep 2018 12:45:42 +0800 Subject: [PATCH 014/129] build: production --- deploy/production/api_server.tar.gz | Bin 77863 -> 77934 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/deploy/production/api_server.tar.gz b/deploy/production/api_server.tar.gz index 31fa6aeadfe20c1926de6e8066608da4a2caafc4..782762c7e8b47e1cc26cc10da29bc7295ba39261 100644 GIT binary patch delta 65535 zcmY&zwkEc1+q|)DXJTi9iEZ1qZRhU1-|p7dR(1b7Rj2>-!Gp40 zi0fSlXrgEskSCk+MCeR}<>74x^VQYO?X6X<#9BVEmX`WU2MExWMqNmLNVd#r04In0 z#lwy`-zw0Ff)lvMWQG-0nb59MGD^5p*#yu$@1Rh?vh1V3*5{ zka(1kkOu&V>%~SE;v7&=mBtwoW>7#7zfspWrY-h}XQRezh-NrJKhMrwLA~or_!a232gz>YF%0UgPu>KZ-F}f?&+_7?(=P1` ztzIOwfTa>Z55&ercEh1<6U#^)^HDaiU~{B5lZ8(+f|Q$g#;5dV{kCQ1B@gft{|g$v*ozvW2nsZ`{v57;I6psg1O6nNiaDVO@B~8NY`=pRbE*d}`8L zHpDYOM4djug<+pobiCy3Txj*+1xKY?8~Z7op+Zb1Fmmt0(zXLO9jId9J5fG>}I+ldW9DTNPfZKLa#Vd+%S&K!o| zUag8x#F!wWt@WbEvTVAO+u}FIoWV^SRdzx>+jE zliK>@ecS=+di6Z=*EPTfyW7vtrOwBrbA@#WaHJkn$#enI^YE8rY3wTh;el$>yVK^> zEsr^4*Cxx?Dst-?IrK;*?$)bsdA{KVLjlS=W(Z4CVZTW@^e4T z0J7PWl7wbRV7Hrz_wpNjPD;SN?)vo?r+)NK3>(pjmYjZO>0DAx^>nnhZ*WbMjy#`su2e zQK!Ww%vQ{;$?i!FN)(rr#FH2x_R_vj>AhEqJdTRYM#Wbiw*uWkrh=Sx8Vcgh^P)gl zoVgac+lQ)_;AMSDYh&BEg-OmlFlJ4ms6{Bv-Y64k+zv2eD`?T4yn0cZfdkt%AI@UM zshJ;QJ;GxoJ`Y5=j0Owiaeo8}qxW#AfeysdGO9wyInE%;a|@N2CKW^^z(zVb4^`b} zhUxUeaEY0wauI7H>%%vrBxfO{u88Y&7Y=3!(^-zx@q|VEsbHR`X&DEJcTj(>bIUo`<_u@(mLiR zd_W8h73dNp!2{G${P|~A?Y?;C1R9tx~6YtyZ;$`NPtw92^;62>oxxc^?Sk7 zF%JaFZiS0I3ypmX@3nPUTvr)Q7W}*DZMiq{2=XsDUbBc*P_J~@iQrumh|OV$hT<+! z76b`Jj|Aco#vyl}@s-(^?xWNPz`^D!8yc`Kyz70Ob56wzT-#JQ2fVbrSbyvc@%4OO z^aL4UIb86&p9J{nN#|ok-D&LYV1D|TMM}E=Y!f%tsXG@2L@0pCL zfO)K}M^M4JgnF-gn`G1v_t^Bo-sN%nMx=Z&Tsato8#30KVYoN*8P;GpNG}OG-ziqF za(TObt?Iy0Q9xCEeEf!GV(5wm0V^@(gf9mNS65s?j4;Fz63W*u2G8Z{6Ja6F#cCcx zmYDnzl=X+%7U$0MCFY+{2GG06*xoVzu<{_zN*a_wH->=6dNl|tyh)o<`&m2PJ-Cj$ zW(RMZ6=)%|sPQ?Jv|!RjeNo*ZOWm8N$@x1^vAswSm2h5isIxokbtb5OBAcvEICVo)ec<(xJR9~;?02~CLzUeGq!vi4-` zjW_ZguGR;2|8v?}&LE`y(r^jOSOue%)d=`D2)688DtLSTIBZj` za;0$d)4sh)c#n3HCc&HJ<#qoOtG|~rg3?d6FhIQ3>=9XEMQ8D-o}^KN+zc*RAhp_? zA7KZ5U&==;t;OfEL3mA01*GW>1__!C(Q>&z$m*s9CIQDi>MXlaxlim*7Pe95UluC# zjQq)CKG=lBoQW^&#&e^i6b3zcG|8^{o~O0+@iN9n+&Hh&OdLLG z_JHW?f!V^qi+v#zq%0Jjuu=RDLaPkP(yM*tngD@yeS=y7z_`|~m#=+A?A72qwFk<< z)LEp3f$u0{rT&UVHhiOjJ$@vwaEdj?YmsEN@`KY-$VbKin9SN7B5br<)T*q2Cgbl_cM-PaJMBYvpo%}@0H za`1WqOu3PuO<@9i2<~6k7j~x4jQK3i?}OLTFCNtFr>g}mp!K*Ws^E* zJAS-W;k6opHlp^m+bGP|$l#`_;AG(HXAtUx{kfMA|4d@pK$Yg)LRD09mBD2u6N};X z5{mDjiSmU+Hp^fe#6K$ftcZe9dE0mPfA^YFiK z0;$+IDR0eQdY+TtcWPp{a$Y0Rc9KYJ1`){C2!LvI! zD3YZoakHC43os2`XiGelr3+s4SM3VnqoA%xh^i$bsUPisqvt0JwOyaT7^+O*UVtO; z8Fe{uikvEzgl)@F1{Th% ze{!ly6xmHPiO~ZY@Id)ddwo0wID2(O+6N-s3yuQtnm+&SIAy>$LYREX6UM1UV73mg z8<26I`Vu%)4dmtR&>8#QS*vOcVI%RY>CuIwbyT!IHON~ zpU}9K@*>&In6(h@hQg%lwS_@bs$bzv&qfG8)kz^dJ{biv>*3qe%Q;`}AQ7#DsvgC@}DkhdF0Y`qj z$LtlnF9EFwPhjRSZXVDnxda+OBh5ck%&y(ycTFoGv5l|yXx)GIffykEtxbP2>Su}j zd3(AzCn+SqA@E}lo8l82F}~`jj;PQd=+l*Dh`E_~^01QD-Y9(+BjB3PDQi{FBwEGy zZ0;&l`*;aExZ>?5s$djv=yN>CdE!wU5}f>;FlMb3=4ebm869+mx7RVi;Ci8Oke|ib z5xZ?)*z{h+B4b{O*#tU;GRAR^tJUo$S7&{T_XUZ$`UHA%fj(mh@xfHKD13HM^@o&u zu@9y=9HOo*CXDzVEJce)QX;+MQc5B{k7w~p(3?58dEE->;J+OJx__^J|1u+ZPdvGl zfz6~9HG#*Zj+7Fv;I|myeD~pOp+m^vKDqH5KG2HfnP?-s>1zbt$B;;#E`8*FpVA-u z(z#8D35C)@gq4cVM1hY&;Wfk#vL-lTrGPsZmGsvpN`X)>LGv52q*lhd~0Pdl(2dWg0n#C zaSzi=m`#r`N!R^&7p?2t=_y`YT6V2C?YXuSyb9kW-U57Wl1A!UZE&TQn!J2giXG;5L#8zOX^oJ+A!YqhAsMI8zr zS=8A$V%%+G@Ocj1WgUZH)4sWD)um5^a{G>FCc^K#up(q@UXT#kAeHb*2uxEY z_FBY~-M-aolnVq2D`gi$kzK{)YGl+;4OtfAF7LE@&3S1+CUmYgc^yg5BGcyFV~%1{ zV`=UMns&xof3fuo{fMQ6j~fx^IKHV)WG*3l9w(oIFG9AKtid|H- zuQ4Jn!R!e3kI10j=28DmRwV?}{!b#Z&X+ku1bSb@2bdE4W4YIiXSs#jH%*(e$qe*; z7`?lU!n}Bpo0D0y3*)mGSAY5eHtc*MNH6Q!PV0ua=)o0{X(^o@I$?N zZ_VMrY*Y~5>NvUBD)urI|F}6ONO6re;ws{0wvAX%C9N+KDt~YbH)>;kK=6Cr?5Ut@wa_Akznu>pDW z&?eMaMP$~rtj8b=XXjfXaO+MedFp_sE{q9qj8GqFy(W$k`nGg8q`I$PfwPHgXa-GF zC7ciC%IxQ9zCgH*Aa7jFPVmG+d)sHw=33_W8x$qo!!4(Ae%+F-g`R&iFSWUUr3jq( zEeB8srYI{Rh~}yz2NPfT3Y)KdY>GICo?u{|YX3|(Od!|H%;ZV{<;FVjGo&i&)l2IO z=lsbUr4P92_dJtR%p5dx9{MKS7)~uL4X+1p^W#m4 zRD$LtEWCDi4GI>_#=R*|BbFhBf(Lrm!Z{QI=noz`J*am}7@KLzql)ta;xH`2%EVNW zA`B>I*Lrzas0(9PB9$_lGsAw@teSIR#I0%l+aXmRZFOd|?Actn49LEW5Rz+8&H)k% zWi`qJb-r`YP^|HXLLtnMe}kPQm{3dz-p@u6P*1dF1X!fC6RTbw!XwzwYmyo$^r00| zifMFFGpee4$9)Q31@vEn;Nn-=Za7cX(+_og-?ZtTpeBeEO8jSu6 znrFE-4mJ#X>*-Os0b<=_y!0>)b`m3J6aYP5&OZd_Na-6ivd?dHKN+?!#1X_yQNX>752BiR~s+xwARN zj$#j^`2MqF#E;-bqeeGRgr;@SfTXZ6Xar4wAx?3!{aY#@-_Nue+g5O`|ETQFZC2{8MbQljF{LE_`88gpw z^g?Xs;E}rxn9?=zsU6SjR@UXYhBsfkNd1{e+WnL1&i_>g4S)TYEbefLtIZq&xtjn=45`XFnb?ZL>LW< zm@kSW(C3?h`V_|u$YpSQ`6Z15DK0YNai`Ua#?ZVzbo9Q zP@Sp^AiPEp3yZpR|1Rk&lnN?itKMKVl?#gFF_r;Xs;I4ET5A<+{t!Q?%o=2BmQPh* z%}M89Yd=P#ik%NMc6g z=ecX0-@r`R25O9Hiy`g5+lVBGj;CQmwYz>N5zDS2k}3yenmTZEolR3;S0#t@l47e# zhKJ)(6VyH6RIa_I#cs`fh}0)5p28&e@!|rI7M_yGvf2JhAqMa?tN2XABi<8w8O|R| z#dl(=7DNjduFOxLyrR{tjOuJ%CTL3x6?V3o39TAmx!DPk7KQx?M+J7DGAdY0CuM zf3Dp7EEs@OB?DVtlD+{R?k^V~J@8(oT)@z71z4aT0b>Nc#rwBS&k@vox@TU8TVk@$ z?cPSP*-^7&YJ{_^P>inFgOU7DFr5>Ya*&EryCX^85iet%cQbVcGH%VmXK9gy-1V>d zj-c6!I)S>@Mju4Y?41u>xd#A}ZQLoq(@=TpNY#QV1mC6 zk-60_LBt%pJ);NTj4w?&c}THpc}b11V{GX;Q}eeiNf$7d9HR< zl7y>{!s5`g;9h+AGc0wjwgskRm)nz#9&0^nS@{Bz6V;R#0Rl>hbfmS}qKS#$gxlow zRpfRd3FTxB^BGas-Citz81(xl5Yp@ykatD|OIG+wv?5V-Q|*B|-GYJaL}}t+OloT; z7X?lJvkP`xIvp0z)yx&fzVCqET%x{W@OC%N14P#*hOx>JLmTHIM3ZpQUyyPQNERK-g$~NL_uj1b=$Oc*;Xn|LVZVERfl23Cw2@9-W z97~~UcQgTbu|kGs_JX?_8mHrGFsL)K{0oD<`D9?+IWevlh)cbPeG&lSQn|BPQWD#~ z)&{0uurNBCf{X9Rm4+92$V6|Up5IFC3PRzmvYRQ$EA8ol19G39>hEk!Pt@HD{Tb; zOSPx^B6Bat=&9&ddU$%&WMZ%WA-iQkod4lpvT>#oKXqSRxgfmF2ZLq)@XYrI0+Gv7 zGnn(k$1{vFM}_A)yYq#=l-`zUa@*oF*`8OdGas5N`I>lj_gdKj&%3?7l$+hW?by-NE%0F#YqJ&HyhsewArFM-i1{!88%fidSpa#&TjQnX1%{ahw$B&it z!58{(z1CVxCF7z)yFC1s+|)>3zB^51!;Y|gHJaatf86vou}1})-qvOdRg*xnIq6N4 z2hQ;3i8V0x#HdolX&TnYSvMhY!!nN8$(Iq0eD(pk4~y(HVfwWYnRV!K+Ue3>@7ft6 z%y!_vZ5IwY2H^0XZ|fM{DifDIQ?5y+=#$(~GX5Zp3U*Z?e?=jEnTDV2+)|%DR#!Qb zQaj9zU%9-J-G(X}LoknJr`<3z)zF*~6(Gd~t{S!l@|u>UzSmf6T$`zk9G zlgTU2171NhdU2kSXllPhqf6eyc~O+RmKXI6NJ<1Ap8-uwECj89O&uB|0KG{S|LDIGWmP)TK7SUKG_x*c?)tUwCV$am%<*034t=HcS)h7To()*+ zto6zo704byW>1O7ubYztbMokQ-YvnKyfRUYo%vX^2Jy;~a8l(#IB=})$`GDxCUz~V zTY&Q}vINae1I`$Os`dk0JXF4c9xWa=Zw@3g7@vYhAgYE}L^8t>2Bwhb?r4~N*oVI6 z(FHUb;Et(T*X9Ab(awLzmBB;D?E$qfV=Q-6D`CHobW$jf*M9q|Fb*9!Um>rR7du#n zj#ADq4szK{&!fXgqP$7x6$`K;@5NZFNRN+e9^HQ0;U^f%@dS8M(`ra9II!#}H9(esd!6VRH264Pq&xp{~Vw5W=9=ScecU zZqADWChk>jz(6@-l0^`@!4y!IqLW7^uA+XDW~74*FK>(9i8b{yKcc1;ZN9?pJk9sf zECY3*-RYwl@lKA=BmSAsJ2H)$`yv;ZGm`lrIVrX?z7K0a?{ zH%yaw26~B7TLdUczt{e9Fop^@VF>m>J~enOmx?{s$HZuLD}%!UnS$%Zu-UXJR{7 z(#Tw9(mqxPZOW$D-k15DylwN!iVsC~&nBn%CYXh^3L(7vFX1?)@AooP`y z&X~?@?KUvT`-l|r$lg}oF-Sc!u zMnEwJL&vBx1f#!`Xs)>N@=PFVwJrs1g5~ioSY{`ed4iB7RJLv4U50rL`!e`Sww8qA zy%6VsKHLcQSK`)qZ+1rX?ygz~y+lY2r9!9}E&9|Y@8Mj{&?q1h8H$NKxIOtdIAENB z1dv|@4WX11>99A0tZ&LG8r5_+H0HjHlQ>0v{dO%P&dHTIty9f;{Omv=@|z&{3XQ2A zT>O(uw#ojCM7YAq5K8Wwa*17U=!C@dR4r2C)uJhHI=?oWj2#Eklp`adgH2ka*2DiF z|4bim`eA#`H3Y~}l9roMnZSxV*YzMD^C&3fl#-|3x(F$4$S;|aw|wwT+=>$sQw{M} z$P2+Df#oogZ-EJAJ)>-zQE;~xD0D$jPEp>EfL~LW5{jR!@G;D4nTn3vac+}ASf!K= zgpV0Jm-Fo}QnC|0?kXaiVA`5k8zxPi%Bitr%86;0-v&VDe0Ia3`#{vQKJ9;Q${P;8fopn&j1|H3}BRih2O^Z`ES5Bhwg$)T^! zA@iSztEv6G#ItG6f&%imF^BJ59(x+r*=>xg8jFoCS5>jjS zokHe>`b4c*yN3`&xf!vZ2TQ#u|Erz%eE>!9mh}pUBy4BdV@v9=eDX&aiWD_6J%p%| zUqN)i+iT`nYC;}b2V9ex%O?71aCBnL$-38g9~0b*8S0a2?wlHxm$#KGh7v~A2XxjD ztzsB(RQJ^tgNfaCJ}Vu(%yvPP5~y6#%e?Yfr^cAt_M2qkG2y7*(JLKpp)}L_moUT* zU9}j&xQc2pZnW}i`Ev##%Z^Jb_{l~K%d5h}Q}KJ-VcF-mhK|h+-z<{QCyPaO(Ijx; z*#G_;xV$Yv0Q5-?(Ie4l2U5D|`UoH&J`D``?9ZTURr=>mm-T=4>|Qd)0vrEv+%dq$ zrmlZ5iq@jb(NSc5+ux9dw>I)`{r0Tfr~jYj{=X!5?d?i3kSYgImIYi%JbE#A2Wq>2 zheHQQ0sr7_{BJhd`rBYm#|m-=II+;?s5kvm-7Z^@-`Z4n#KlBICivYkVcN=u!;Zf4 zu!Tf6VLXH%Z{ag@Wqy?OY{iGJwPt1(J$d6P(axEbsGvC?`lh@*qqN+rrIkW3(Duu~ zGcz;^Xa9*BK(TIJABjG=p2^mxk&UapAqiQW4<+ZFs{uFq7n_jGK>MzQM_i@m+0(s! zDjO8H$0C7}%mPB?Fs`jD(cGEOrJ;mK_ zu4+)2QJ(2u!ly352{xRU8~VO$I_DOl5nrx+1b$6%QmaY)q6Ml9TP_NZowACp%FJLC zU`8YwP-c2Cv@joWccYbYmm<5!*BDMF-A5u}uF#TG>rMRXoRxxK2+oe1M+$=!UEA*+j|O-8v#dJY<96(4hBWEDn^ z=2uN=LSDWSg(flJH}Q}~b!Ehp`qY zwP_i-G}Mj`J6u?BNNW1b2+jT^ny3+h(Dxs={E-jcza|=D#>l^@;>A!#zUCsisesW0 z*pd(k)u?yHJPp?5ekFMJ9!6=glg~`0TUYckNf}UUFQHrh5wyCP+>#SEewElQd(#Aql&{)B~->H?0)}qpxh&W z!DG|1F~Oqz>-3a7XN6Taw}{P2CDt8$s#|miyMyMYJU_Q3V|ZErkn>f%lA@bCP0nDj z$#rfy!8_y(jyHt*{#p+EKAh=r$IM@$#Vx+D&~TTxJl~$}jLk0Ny7zC8FE$M%;AD9t zoD&Ad|0C@p!w!tHfZ2}^%4lt5lgH;^cUosPuO7aNIJlOU@(!N$%S9&zxMa#TkeFmmuZB zdSI1Scl4G1POuY>m4wkefq3s7*U)FEb#4&pywtvxUu-$O}f6xBJa@KtLlZu+lzF=bj82=|zpH0^@ZxN)SE8WuYQ z|6ZvStby9%?Wd!Jh{&8XPvJ{C0?q!3dRlnK&^(IGv`f>EJTv(4cUH!cf|im~)fO%> zuvEB%=CYYZbnye|sWlmkAbZwiIhs>09OYsiqrLTt=w9+Wz)&LJo4IEf_1QDL2j_3` zCo{tw0_Qk4%O{R|Z(5HioRFV(bV?`AC)ay4bw1n78f9XdiR#*vXl^66K{aS9}flX^)}N1Oq1 z3UUw9d`PrK0AWq3@AwnI2;t>P>q4k&=i0s-SQeY|b+Y{Ze2lh{Ya#lOH`Ds>(j@GPMpIs273mVbV`4TZS&Gv4^1W zKi&!c0gX&eU8k?RAVH0L6})Ik;?(5uw*gj*oOzNSm zN2w9ZUP|UJM8P1cEq~owNyRX9IU}>KzsjmIM@*uAH#L&ouO)c%myT#Nb4a_|;$pcR z+{N(_@TY6l$LdO47cqb%mJbJUR9)mIGFk2}(xg>e?%B6N2JC1~;HRM!w4y7~Ht6ax zNV#eDe4Wg72D8EzIyb-YqSfJ*KKIq}EbL-|n?lNTl{x#m1&Cp?QLx0Eo&4Qr(4f+wMZwjqYiPSFi5;ngX zsrO;|lVsc~UHDsO-d45|=pyW0CRnx!nYISJw7sHu26X6rG=2ROLRh^3JPxtTJK;l^ z2m2FQg9B00N){qJ^+?up!-S>4I9G7N@2$q0q7fPkE1D(H7LVxS#?<6hm%}6uS&*9m zfp07xYR*d23IcuQIFO{mKMLLG|GHwDljWEn!(Ph6LuvoL=^|blB+o0;1v}78i<8fz zkDvT(WgunCqAQcyN*XgxG)>EAG_`a2BY=bX0+S^V&lp6xJg_my783SuSeb+ z`dAh#qW1*6K5#rGXn?`n@}GZGoyK$H`eL|;8doX4NUnWmbgV&-ccBg>`B+ymGOSCq z*hvUU%XIN=qJ7v++=1UeN)a#R{lLUjc{(Rg(7Sp$i@}b#e&8gNVno@*G(O%F#F9xW zi@$$pBFZ!eQZj7Hc0tks)__-+4z}bkfR7Eg&zn%bg=?VwEwkd*J6U@S#}S8OJ2?Fo zY*dU^cHb;wQ>Ra<^;cpAI*-U#{PiTrQC?-jaAP<7MC4s+rn~mP$;M*^hs5V0ZZ<6( zG0kNsJ4$kF=yIUv^C15F4O$R*_9E#2cr^leF3JZU5)lDTpnCwX7l7ASN#NN5(f0@N zJ6`Yz$8&x}@N-R4@M}*J_=PO+ncj1PBh6tPVwgw6tc&l=YtbE&mn4wd`s%l*E4u$} zSjoQY5A7I>M^VnS@25Hs$qj?WNSc%yKH>RvRGuhWQTe{&d2_N66mzA*@_j2@>kO6I zn8Tt*D4D5!A_K^8H)9)L7^bD4C_>HDFnth|%}QjN&-Ujq>p%3GZWB9DJA7Dz3zLZx zJD4(vyvj#Yjle6$F#R-5q+JTq?Vcdm1{mXI_4eErpC@lGS4-#)8!fTj3%1Q%b{*8fknuvIb$U_1n^Jw9+Mu6#prqv#P6f^9_D3~~Hr?5XDpS{21V;i)Qr}{>X(QfMqM1@801oT)G+>2j=WLU&I-8>{;T)=?P}v)^a1Kt zedb&qObA&5y3UpDU`njM`p}Q2()~ENVTt8KJLB*jtLsg<&ibB2h5a;wh3~jETAnQ?wTaaOBXOqa7bPO?AQ<&HC0O|z3al(?58H~lT^U-}OM+r~HFHTTM~{$@); z!l7wMLrX_6&M;N}rQDVx0`DfKRJb@bOv=i(&tIz`VxDK|?LPWuR zjsGzPwP*b3F$XCkJi&5|38`OPZUr%{L1or9fhKmy<5%59B4dN7WMnQ%U+OhDwGn=fj-$cO+0KFh!c-K{KF>ocfCingOzlQn*d|^sED1id$Q{w3O zfF@K;GM1uP^+_!S!-v_I1P5@2P;$`6PpIldKsrjH|zHOU2*P&8z9T# zWW4N=$v{gb_;@p5bx(95?4YS?Sh;iu=58M$H$F)ZSXI^is)+TP5EKV+)X?ej$r*#y zf4*t9mYfo5n=K785`TBvZI~`@fh{z*3&Opd#Hrhb>J?xcEqy6*2p-$dq#3IlQ?jXP z=z`TOPG?PnL`ARz9vay+PIzCmkv?Qcjo@*bnwGYY_=i4#Qif}r*T_)PjyxP+GOIv` z%NvR-pnJ2&^Z6H$G!Jn9_0LHKHF`bchU5fuXbXI!GGdc9OhXPkrGkAqgKBRdxxHhP zR3ME{SdN(PiK0E`2{tPbZry$gR6&%>JfTm&fK3`}@dl+69OdV6dM)l%R6|O@loeqI zr#G&9Y(;8luf13=Wyr|=rCj(sI#fxdU!=rQ3}*sI0ZcCKZw%lk*qQb|`Cy7_@PZ7? z?UZxt-cLuro;K}Kn5s!2iN?kx+IxCl(i`%EKPxk^g_D!x{H-UV?ELd&q0aMsVf86_ zL5J!KMmD-F&Trl51^Q5Z<4h+QX~R2!$%)EFAOf3C+QuDo^^P@Mv$}pYO<;|a3@^sIKSM^$>L|U4U`PG<(VeiazM4%L7 z1Cq0l=4?pyU9wR$RIfV~6!i9KOf_w~Nd7USnRl7?>JOL!D_7IL`~IMQ&`NZ+)z)hv zuIF4_Km`|PA!78Z*um8vkkT*QlW%WNPRGU90o9|Mk-xs+M_-N#Z|mi++L&TuNsA5x zXT=_VqGBA#V8E~lmBzpcL+6k6qn80h>ceU3RNT7Zo;58dUDDzA_WC=>%Ba*z@?kGd(;k@BHnscn|E)qh(ZxnJOXAnVzA)dzoLlte%MkRGi@a z$P4%%C&@oE$$x&57njhUypJZzXo;^yg)abKDDpit#rDeJ2P(h-^=u~MrBOQb|5-^i z#JVj8KeX=r#B0K`R{L&d_98(GY9Vg z8)^@r!G8(!_fN(fIg#zVJ2C8G=5cm+I>vY6)9yn9ELt0GIRPmRvYme-U&y{|Ap_ql z^8X!i!PFUGpqaEc`9pB2(cn|vejJHu99IuQKkppO zahXa6!WQ=oxvi~-bMG7SACRYi>cr%9|Fi)ZlRzJ5gkAoP#jKqbyZp49 zY8_u!)^r2>_SlzLYKf*%Po@yy_VS4TBPBMK6)!cw`v<^i;AcayiL?O-;ogP$NprlPYbt+_Lx5vy|di5 zeY$DbcSHjx`-~gtOqE62oKc$7wsF?cqh5>*Tqj%9&~0Bd0*cjtYSeq3U~b3J38U~1 z;ur7~`NvR_7MRQ^(<6BB%|MHIr z#u<8LdwGFs^~$z4=9_g|3O6m`mP2MZAuqTL#>5M1wi?WU;5h$bo6KTZ&S$Hb5}`(Yc)Nl;L1<3E7%$@ z_8b>B>_zQq9ocq>{nSxKIxShUu|=7Z zs;C!`eykiZ)AB)iD$0wE_uuRD)m-b!JdBqO*nz3SXi0sX!4Z1antyg;J^(sNen9}y z3wo3=FbCtj@DOR9Pi=g{pDE%IgM2jf5b%}E$%G0B>0ENdDnWgZj_-szeJBw4o!@x! zWq+!T*lM;F7kpZ=?86wkd>5{1acPx)Bl=_p;zY#vCi?zXLtYKgg;Ov3i4*u6Tc@AN zPou+AA2w+K0CQojB6B=_$q&d2`BKmQ5e6d4v2uXgT(74!i3B~rSsx)<6A?A3VEDzw z5L60Qa_F5R4gyMEb!|CRwODV&muWqj#2DHsOKznT^oPne`Vvl;;rXD}xKdsktdoX0 z(k??=cJ09QQJXYGA(JgCjLX1f0r9MGFAIILEM$cJ^*U z8ZC>p*+eS9JUY}My-1#dt>%%JHJl#q0QQW}R6F>qEW@r->`nFHJSGY~7JMqp9K+`9 zrwzYcD3-N(s*c)-W@Q#a0avp(Wq1;Z;yiulSqet~B%v6V2o=-(G{S?yKOC}! z8Ju2MXzgJUZI;v`@E`#44)|%-KZR-6vgZuWR(h~yY#jCd zGfvN5Nfm#BiHS>6@a*1Y6M%xhsUUERSV?;kg9O$W%CjO|utb~F5n_j>C|=D^*LU1zW>e`08Oy=d9MP@(o8GD9CDBQtjRKj zX@tn3rE=NCk7H_3RpQ=G`ezQ4Rc#rq(I6_Y)t7Hq(2}1q(&;xzuhJ-UBv8B(os7Mc zmbk}ISU;M=p_yleStYP^w1#r`H?7l{<^HL3dZN_pInmN<+~*-{EL$3goMueuIW&-} zX9jn_+f4|VXWa#$%}s_0pcg9Dq8vh|gF?y=l@M>jnWE6_8?Je$rOX;6;wLuz)EVy~ zrjepzC060_n@mJk)%{uYKs`@j;dHdP8RukX6#R31giWq(M9nEwORTHbCTF@i8dGTL z9945KkDDm*GEXLDf`BrQyIJ96bDC>Xdg^aYcu8z^oDc#aOB;}Jl}o)Zq-iJRL#!mK7G1(lkrP7hxM=dew9_# zs2mol5`P)~EF(^!L>6iuO>F;DOXuX;*{F<~QPbbfPYukf(+VahovJ@W5XY%B{blp<137C@rxmDN^1ay zRR;KLEob+&W-WH|>1|0YSN6qlDva=qC0x1@g^|X$SBfhS7)N{_Z6GMY)%L3>z5XJz zd3})jJNg`L19iVZCZ&+ zrwphJ#@)E_6KIn-V%W9T{V}GlA|{?C=Aa0{bU9lgV13XTIVz#nB%SKL|XGd zux$lP9slbJherEk{SdU7beOnBO;-+UCQjYp8t!CoS(3w$uRvNm!XUz|6#U2r7gZkA z801(VQzrDt8*7fz=j2cAP5$~#Mlo#)j8I zph_t>ada$IZ7E^7pGI$RCG!_!EcMztOfN#gfvFxn!_ezDMiIh%DdCzvZ?{aMG?c1t z!j}g9<)g0_D27wm-_UWM3MiLHIC&8|f9^X)I56) z9VtF-0YdvLV7NuWpUz{U#9Jxb9c6r}u=S~_i!qE*7g0+XWQFBA3%d0wgWqFK>(i|3 zPCA-TiA&u?ISMFx=}rn?Al7?%f?S5gLV027$@8&OqsLWMMbkVcFq$^ReMS+)al*&U zq#oU*+@|J85PivQQcNH_5|NVwj3&f`#ZO%*0H{8u{C=b(5AQDx_X%x%=({3G75D~7 z!l^Ej6U{fUp=09Y$I^@HJp<=Su9S9>x}nBd@l&B?T(D8R2Voc{kv=H6r1L(WDrY9@ zV_oWOxa50e>7s?$?w$wxIjAT4&k#emMU{vselQ(hs*ZE51jIHc_%qN|V9I*O2Q3D< zf!gjOJOzSB=k6d&D0f7?Pe-Iz(G!_A*dA1?O1!9nSC_I{s^*?QshFK(s5C?t;dGJOBx1Ol z<`|2TT)V}~%@3!L5PxYozl>Z|j~!6S0m!F{>@3?BX#Jf1`23-EtzLX#jSDImJx*wA zR_Yr<<0Emry$sPJD|@9wzW>$$I(xaIv7gVrie@QtUap)5#?93BZ>A6h)D&gzy2C0i zwgOSx3r%+yCO)aG*K_3Qk~g_z|q6MEDcvVx+Bal|l)q`(t*zZsoU; zb@(xq-)>*@bF`w?WfpI7O?+=rI1u;rB3@->*FxG_`aA9!@*+(8=dOhZ8krt;Zsw*y zE-o@&x3?CRXFz8}?dava1P}TVP&BHaIyU>6y<3F)-7Xjn(=8Z*{jm0hog=Dh%aO zG!4b;!^tOf(gB04=faO0&e6XbUOL!L9@UJeRY! zgB{maNs|?90~N%MA6PFzOl>rFqkfPKap|Zlcv@=M!mfit`PXfLdqP~8W!L7445tb2 zm)Sq)(|lb<9kK9uzXuCGhJ8*Z8iQh#ZscUDD=?e{ok# z+z9MmSCBmMGsmaE%`kW4&mVK{*Cyv9A)!iUMAOz9M22a*Wcw##p~ht@gW4}W>{!%? zE)YnX%@hKCj;&1!2=m41Oa-7WLW-Tkc4%943_!wYOSmT~hC{o*$;8F)$OIHhRmzc2 zYPL*WV)Of4(&)!46+tU;Ci;0{#s?V-@J%V{3e0<7IM`3^hhTPs&^3I94{#&_$_qYp|stE)|>HOmO2FU*8Xgvmy=mryT(~z~a zL;jC^^aPIEWynKWi8Y!Es-d*x_-ww5Y^+84_oTckT2#Sgn)YSUSr2asEJ2Gj8_$j; zA@RbmeI6tsXvFWm?^YV+IZkuhBi1RyVUz|ta$S9Ze~F-%YW^eFhTz?Yjmpg9CnDiD z^=n|3Q&@(^?qvVwP45)+akL!YeIEmDA$W7d|H%w$<#|1;SAN%x`RnVe2U}ReN-g9S zIr@XPeUihOLe}D8UanJXul8mpK^U(jCa6X>NbzZotlPja*RI4HBq>hzpZJ6QVc!kH zfC@A{b;x*o_eNwr2#?Ebn$nNXnYYI-#*6}V*b`wSc>MY91@2W&ZcW6^@=BOTsxcm zYES;*^ru&l`b?M;7ybDRQ8J?gApjW6^hpfxlylwADVb=>tveh1Q;=VL==tb=CXAT3 zdqkPr5J&%3V><*a|M}~92BszZKP~~V{nbJ-Pkn_nedm!bxM3!*8h4J-ug@=V@h4G( zhP$oLKhSndmGXp2@@m9?Qh%LRHw6v9-M{%jAxPR_vxr(*P zlqO`oKc8CIqyBu9BhxcTMnVGh3OEh;yg9xve}hw+b-n?70~-jgM2swrz&DY{WJ^aj zK(VC_)RSz_y%U~)AvSb265|hl(hQZk=yz??&y}%IJ+!%L$-L236MRAAI>ZX_L&e^t zf9Ywm{0E+&oN+&q$jK~WgTfXJXNb(L?T3Ip^XpnuhI4m4t{8q26xrh7qb+JB5c)K1 z=>IrO|4C1W0IKOw{nuMwr_Q83lxiRV-gA4%zi#BLg@J~O<+cgaOT8#}SaTwB_fOCc zlQY8=*Q{gDG>4DBj3G50HumD_O*EP@J=c)FQW2}2)Cl5O*<;`9{*a8dkah}EyfJfs zD$3d&E5p}?DQ+gln7<*`5^w%2?`BEPjro-}_tCqL1MvmIzM>*#9^1q$bE*N5I^@p# zUelT8scyhlOk?PI+&uKG`}(DrE6;%aO^T@s3v`vZDzlW~3|_NBHXi-=J2r-sFSlka4MsVo!s+9%A5rI&g7Q&30IhF@eBun33z2{7 zfCwF^_Ddp^e%Q*upyi@d=C6EZz%6eGyKNsgaAmpiz9C$ZAmE7`cu@k6!Mo_52MSLL zC%ubF;NdzCGz4q@u$jsd5pg!B2hsPj5ot*I@yA``7~TZ1v>tPr>@ST-*nKMs6fvDT zr@<>G!on%(J0291uqFyDfN2w(j&MJ-^uBX9nmP&mHCrC}%_7zL6K%P+ z*MrAMjzIqT7aO)`cG@Rm9yrOPY?y_u0KR_P0^8NfiGE<39F)!}d354bc3IQ@XKL1M zK6VF>%E{}S6aN)ajZS!hMX2P7vXAI~^dOP;u1XpEiM?ahvOlGlsq%?1Gf zCLDvG73K&=0JY8#8cVGfjEkqabWZuzH|x64GT5p`N!0B&g*QWYF$e$VBec^fO2xIN zOL2ou(k&U~B5YA!7REQ&&|YrG{BJ!@Jr+!b7xs5C z!ZGjU?c^(XwtuZh&8BuxIfWaZr^F*2giG8eWmd_a!Rs&Eh;Lw=m_N8IyEw1BX;_AY zw9wTk4iO;1)sM8=SMG2t4XfwTz$)U+9j0|GaB!zZn>(m*8Lw#+IV_6eiQl!E^+4H= z75nn3TCKbWWb1*+?+4X9Yyr!SK~QgP82qa`moyF9_U%jyb>Vi71o@ndzG1PhT-@cA z+C36|nE#&R@9q6uVV(UWe)g}P-o{z|$E~|l8NL{95fk3!%?BUo{Du~Sn zNc^rhzz*Q$xrgP3465~N$M~QB9UAy{0InH=*Gk|+R?n-_#>-mX+qM$;j|6;TX>x4( zqJQz#Jaj?#D=g*=2m>=Cf$RX^?#cD*THRv>AG-Xz9Fh zG0#a`M9b~(11I1y3RV77+x^$Y^rkHvqay6<%Crs-195C{(dHs2{<8fGE!%<`27Ar$ z2}H<+9w*|f7DGE~U?0+K=22g9Y3mOKX+VgR@VV>pi#w2jYpm$IItlv`DWN~a``7f#Z z4A$PKciPQT#9Dk2FZ{5Aj(?ws>N3CK-)|As8s!{?7UkL>aWLBV2-uw=nZX5g0kHY6 zN~|Sz@I1W&$fC2S$!T%m?^jXpCsU+ou>oxoPuR`2t!*4;+sn>?@c>j+8g(@S)3o5i zwt((*>rG)6K_-mxtV}d|$vUc6!RjA%0wESaRhWiU z6pN0vB|3UI+o8hVDKx;}JvX~|vq}MbG^6$VuRw9YUxu-wvS?okR1^jDP-3g9{k%#f z1M9W1+bMZd(B_~{IeH>FnjR{r|Q(+7=%wmfaRL&L;GbV zEM$Wj)oNR?Z9si}ohJ79rE8USlL^TRn@<|H*->qG?&k%Bm>O1%aezel;pS}sK4-pf z)eTMk2(Uj(zXU`fD-%IxisZi@WI#`<#eG-TPj9eCdi(pyDJkSY8pb&5wSw~E+s1k3 zrLa3boYfu3K9_V3*sRCBpjNj%k8Zw04VP93atZ2T*y(bUP&=icnTsbjdY_HLIG~4+ zBjwm-+-`)8;l$icOelnwsGN%5l$Pp2OW~~7o)}lA+aBvBVTB&<@|N2T!1c4T?^Gw) z)_h=sk*e0ysB-6PtM=h#dg}g{GtR+_Q6AGOxTEMXF7Q5ya?1Sx&>eYi19iK~?kQfb zW>+$tW29Wpq~B@d1zy%1N(@8>;xn zV1|pYU)T#t4}!S>CjvG4te!2PJ6AY#gboZw$mk$yOuWI~pSZ*L`~$ zc<*2eHf6gqJ%Z;~RN*n#gawA9C{W*fGvMz$5cTAXV*LR3V?ck7H|ds;hv+~SQjQ;e zQKgStEm> z5V#rO&GE>_jZ(Tc&$}MH$Pn+@?a=sX{w?)03utxQ`TG7T4p`x59as}v%6_dPp5-0p z$Y2oS9EU{q1nxuJvU8*qlRKHy3t*t|#4U|;_fK~98U(QNxB2?BCb0>3P`*wH(wyiE zZwYZlW1Qq~@UB4Rmb%HI*uu-3&qkMRC!O}Y58{(+^Tvqbq6A#tdCFo@X!gVqQb}Rd z=qY`VpUhofZ#y;8s&Z4O6{O2E*erGP%4Ygw9M=z}3qX+)RRt_noOtTJ0yrnK&| z*+ftCQ4^^jHfOYT7$2L{32qOEcpJcig9Dx zQU^{r`+ztesgHS6em2svQ9y3}uL?!?B*HzWiVr8!Q_=U0Pa11l25+oFCLE74sfbK-L z*=)cln|DK;O!@-|e52PHK45h#^i3kHx7WX?RPF;E-MAIr07u83Sx{fLJ?#L1^${?W zJImE~u93qZ^qZs2JQ~fHFud5E)jle=$%#oaChL4pGQ7>-e2>-Hxg{2j+pPQ*Yl6}P zboa|-JkF*m(nyJ-1zdX#t#%HL{bNJdJWhM=Ea+p5;kSTfEXDt^lkXZ;CWoqrwBpOU z+nTgL4YmB&S9Hm)`C+oaMd2Gw)>{P(z~%iZ6Kv@Qp3k(lYeFH;L#bu^~Rdg#>OA}ho0gh!?#<*w#)Tc0<=Q^i$j?UpP-c9 z4uYPsH~TsuuK~E=u74)~wT9+LD(v@k!}jV+CHwivn|kTZ@Z_xK25fFs|Bo4~CRZDh)4%rBbgf$?Upfe(sb5c`9m+sO{g2=#Z~9YP z+%Ivz$oKl_=RXk0uKbTTbH#`x|HrSH_<7I3u%pWYPGQeX9k8&YrKe}@5MZR>-R}X~ zmyhx+-|pLE-=THZD;=ZL+V)}cRQjR8vW~Y@!|7tZpK1qeJ zQ=VpeM+Qf{P;^FECJfpMTI#WZRH^UoOJ^kf?tYYc)*yuwd>#3OLY@f!j6Y1a0c^5o zwuMZt;uNhEWK%KPu4!5WkpO?f{uq6wC%}nUI!b#GMtC8);@f$yBLQo~cOB4cBq;eA z&yT^6w;&M#b$TWsnCT{$ZLcR9-}9w3!Q6A*WMIwyOJft$3+N7+1#Q~~Q7f6gCw_o7 zxSLW#U@mFB5)qnQPnKEhoFVSzi!v)@q&r$;C_rkfD`c_>B3LA_=iLSw@I`%r1E|cV z-C3i0<$tptCSsG2q7;*jl||h4`KcaBc3nE*`8p>J7w%#+Rv+P265amX4||)J5qHP3 z<$1hBcEUGj44|Yy2#%}m>g(Sfiq&uYusn=6#L^c8a{6JQ?ZRDFC&$6ZKm-)~99<8O z?NlkDKB2YRvxVfT!oFIn153Lg(CooQ@YEBtTL_9Y^AOWrZ<{>Wka0n-iG6GTzQ2=T z%o*l;D6r~h}q*sl_$0f7)A%6ur3%OHmLE**MhR)=SNw2GK)$th_3+P>1KXf$nT2u zWozf0!|Z`PqlhAc7P~WoTE>+pCc1SjATL>^zZJUx1q#jO_?z*7P_FEOZ)LUn#V+Ic zjvA{y8ve`ga^0POPfzAVFIq+Q*Bi8x$%U}lF!Yb$q#(!J^!l4(1Nc4B_Zbvo3YtPo zkuc&lk*v_PUM37fBUuB$t$UJ?A7O~O`BEED$%h7}eh%4;9fuvs4;9u>xpr-(bDK0Q zdbKLEs9f@Jn`LS*F;Cfn7M+g5B~HON@XxY6!^wCie@FCBBI-~lJI7Z>tjg3mIIyB4^-^>iclTKmRdJSa+V7K2>r6`MPm2`PCYJ=G2V= z`c~yNCKg7EgYf>g6;NasD2cB>AM<$T_#kafOgMf;=&9~vo)E14v|^YG-*HYN=Z z*?;W(?USkeW+f~YesRYBD|))cLxIzQ$0BTWXQOrylj+!UooEidd)Vl;OyJa_W51ws zUey|oh3((tgTi>Aa)`l?tk9O=B7&T~u~VmyO7WWj6fi78yk#`t>194!n^-lWZuVa%e-I!!W|s9YlIx%osBx)2oK`vnm&4oL_0=0EAj{g`O*XN65kvK$2Q_>$`1ll~ zF3TBmF3AhLl}fl;>nLNtZE&CVw6y!gg6DfhW7j0c(cJroTK@Y3rkVb$IwKkJk}~{f zh=iSo$p$Xz48jwmZAJ$?nb6k=QZ-rgde~t6!cBdq2*)wN`qI<3@!zeOI@vS2F*S~$ zJ_`~>zd4?tao@Znm!vmg-tjatDEGt%8`ZO7!8M#n?!%^8hFAx^UwksXmgK$P$*K)L zb@R|$$3hgQSxhU`Tl+g69uVY>T?h-?7#d*btG>0Eu7hqKcP+{1AymKvsg=6e2`0TM z24W$s{Y(I!t@OG^6Gc`{s*UFDW?AkhTq)*3gnSB1gDypahrh=n7@tstzm$8teT?=J z(p3(92UhYgJq>LuhX4ze#x~&fm7@B?eme%e8aY8xp>|ok#>bGTzs2FI^#ctmvGK&h zH-Rla^fk#4@R=>_p=f}6>UF0T{7`t%cxOH`W+3G|h{vL*s3>Q3jS&0V2~h%tW4NbM z6prl{RT!BIiALc~>@UPtPv$rCtq)&fTD%lfkfduc8%z1_2eX-}+RTxX30H46tF!>> z_4Ekg`oD~Yh#KO~$HXNIeLx}s9tf})d|hnBMp_qPg~R$9uDB#l z9G^5xDj;Ah6T}kF{*CUZ%?E1pkCuu-Ob!l-gJI2*gWiJqHV26H)aZA?EL7VqOp_n8 zfi$sJyD`YAOg0#F5At2z^wd$XyMq|ilAZtHmAo$ zZKu1kS_h004(jy4C&J-JWapR^`gV=nUQy-_R#}$9+i`fLFxvfm8?j1JMf#0&Y|U=s z={09!i`O3e`ZYeqZbZ8(8XFCnNgm%}J5$kjwKC4+{C9L*J5EzJo(oPlR0ygh2GaN9 zDTGW%1Uop;gH@lilb#xhqM0cG-?*|+BQa{ZQW2nkeDLFNo1X8fg9dq@%$V7$q^_BG zDt?RwC2wfzM@y>swYtzeV8DjzdhAy<^uF6*2t#6`&={JujOyMxDfIbjYFujf&VGmc zH}~}nzAC$P zx@RIsVL^D%g*C?NiP6AAlboNQvc4(5l?Ut%^{S2nxLOQS7W<*uI8bOEFg^E9V0$VJ zR8F>uaDNFn)S8;u8R!g_p!Hew{0?Dg_+g|?4&VJ#S4F}#1ky1ltMefg&$xDH;g8$# zYFW*Jb$A5QPY!UNErxOP)Y2jce!E_RNDY?uY7GKk*f)l{>LSC_;l#&VT%AJLco(1< zb+XJ1C@CBdxKjMdCErvx;hT&=MWs;PG_76?@e8xJwn#d}oK}?|@25*Kt)XodKJT3d zGCW2UzOpNsf-;VX3hgGidwQFmJ3QGq2;#jJM>CBZYqDRoEf(uxp-_C#{1M zB@ldWw~(rU9b8lfLD~*E`f2?j98X#MDB)XgZ=WV>1$52qmg4EUL?D8_dS%lWQNMuG zQ!YGwEA_vy6!UX72Haz>q_PIU;5|Mv%$L=jZWd>@XJ$zNES#q_sd4vuQHAnZk_Ir46E-q0Tyo)UljoFlmdxCe}23D!|LQL%=#L~&F(!^_sm=h3bh|G|?8!Ynjf#*~w@ zXgzD`zdZv!XYyYQI)%PX|2|V(Y6aKUR&8E{nW*07okcsBnifpuM_5PaJRiC}QqiG; zBhpAIzaJbr)?d#SVcXfi636aaH^Wy{9Gi~`RtL>d2c`}c3`S)D?&Qp|d6@Pjczgq^ zFW%O=&LAJ>$*$c&IgJCPtahqP{kB%?Ty1x?Ta$_5(!b$XtrEAr-%?Jis(;q;%m1pk z(8Up0PiFOfrg@U3zXq#U-hQ|b`+hRgxB(h$pMaJ1W3D?uPR0P74q7jNJ|!y~)#s2) z@MhkuUWX3gRWtym9be8moI&?k0k~xssdnqJn6}Pdy1XHkF1Ap+xvZ42LtsHzW*e7n zHAR06p>gcZ(h>Ok&g42)X29Yi*(cDt4xyUhsD>R%0ulxqDsxFWiQ#&zMQS$MHEH)Q z{5yu}-QIDbE04V4aB|FB?%4W{i;V+dIa|H)rY{q;*Z zM8SlUXur$IWld-5@Q6{Osjf_A2CwQ9Rn+B;xdGvirTy?(PjgxMr)uAJ)W+9xD>wYN zczCsKr{0XG%aE>|aRQNdptQ<$3cNcZ2*3?FnbZd`P#Y$1^mDB`3G3^};q&LymqM2L z$k=cqAXd_sFpqdR7_uS&yKCZM#K;J7ZBqDR5wr7SrxATykXQh4`-mFf!MU_CX53=zHwDl<60>A&CXuUV=!|fpOKiL{KWpgdo z&>U=D?wMW@ejsRrNZdIK7fQ$z@_S16@5U_7bQM%PdNUmPkB!$gSU&^6#=65pk9l!c zo&x+D0k;u9;1n$ExcbfVHQ-I>ND!O2?>~k5{|nbk{A;`MX#S_ST!sIM*q`MCNT9se ziw~Awypp?nq^9pv*-7R&@(;dx`%mU^({1?N_~zx3%v^u*hP&yMo343GPWY|B=et^z z={SeDN1Y?)0LOIG$*=Z3YoW-4`V|xKMYsX`KAb=H{I|SbwnH}nj=oMij6etG1in5) z?Z;a&tM7)ONcW(K`sKZ&cdH4AdAQ? zuG;$vTKRkmwy&K!1iuqA`b+S5?ltGv{)RE}_H;0OJdI2byLPRd*Rm?%T?NXtO{Z{2 z;9K!~CPgws0vY%nFZe}sX!9i)$S)`UlTktj|NQWzaCG(pk)K!hOmD4C~C zEF0W!m4)~fx*ac8I59TkLD5II7z&JRc?#NxMJ>?_c12XR2Y^JN?|>zl&?No!fs!|) z9r)mEM?uKpP;XV66?3`nZDsT__8RE6_DZ%N_f)OkfoOrqm&Ys@c6Rm5G{-MPP`5v&fal{lQWnt!$D#(Gc_D$Nzbq{o;n z65v0V>hQ$Xqug2X8!TRHJX3_ShQ(-q?zwHV263=THkH= zm-A;4Tlh6M&bmPg+tIM~tG=O_GuxrN?&tDk2q)&9G|PA(?Qo7`(7q-3O>azOt3#yBF8in}9)t5!bEumyat}~+$yH4=ryJF_{f6MX|$K0XCR|j_S z>OTu)=JI{RkCoDFPyafl(?TWL#fqC7HtII$V8*nI2c^UE=T7mG+$w7o@h)kV6j$Q5 zwZ*MfccLS#S8`Nqei<_qqMUP;M0ZiM$;F|Y5GgDor&Ica{`h^6Bd?R56xYRF+Q)*} zBZTz0i)^ReWz?NnXa9mE?BSi;AbTJ}|29gH`&A;r&Z?~=EAC{pOgP&D4+@cPreU$q z6}=&}(>q%}tODL0(fx!c_M42qKYN?seRB^!ELDHA-J;c_mZ8eNV|3X06UWEfTw|kz zfBe@yi&Dic?5Vk6085YGBj4s~OUH2w7;>HD{ANlsOwf0>S5d8a{_t31_Xb4LbT;DU zZ@=_(9bJIUi-VwnY`Fl43m>>d534zqC=uN_UsSJa*-=&)B(%iDFrI!nm21judn6FPlB8C9zZ+w4_8jA44o{*{ZfD7>coDHXQ9y_; z*B!h$_?lx}&J#T9N9^;&1<1mOY5O(1^k%9(kZYkx*5JcNk-}M|BAeQzFhq?cxY>M% zvgy)lh2|ih$s~nDjquv7h5H79ToFr(=>3vj5Uz z*Dg`_WsKBQ)}~JC=4#o43=LJ9*w-R}h#K8%4(HQHBT2Xqhx^%h7oGw5$lDpFz6_+Z zFN_eFq0`r*CcE`Gc*7tTxi@`Q-o*^eE+_t@0;EtSvL~M|sa3YyT5z2%u3zH3oO7nv zARj8Ax>jd`5z7vZYRNKmMNzHRJUS*s^kL3vm|fr^ZrAFoTvq|sDy~;klz|c$rOyr@)>0I&9W`Qggk?%IsAiPd-?7k3jtw!XA4)hhF$)a~A;HXGDwf*55 zyendV+Zcr3%4X{=zX3eBCm#&?JDxi}fITtlbl~T0K7(KAMqy7i)d>2uaJgxiCw;l4S4$wyC+xU0S&FC#57@eddH)`%cF@-7infw|v6GOWrE~ z6#Nxb;wUEj6-!OEv7ej;ZU%u@SfBH@FKWX@lC{*Inu1hmd7a4^lfEq{Lu`2|6q#Xu zU>IPR33HnkyAEwvU9*+eq2B?d$;Mt4eZ@MnHl^K<;>LG9{x;qhQG}M!{AJ;|A)$kQ z6!b;^xok2e9~XA$w=|5PbCwWWF{XB6s`I*;AALXMths1^e${tyR?b^Lwjq z1A=D5X&>wAJkWnB!=Ea33sAV-d@rtP)?mY${R_xM^NynwZVP1zZZf?gCz3>c@f7-v zUDFh=GjgD8zT_vO+lMnw3FfaGq#bd7GE@TthnHE$OqY*<Js2R(`!vtS6A5EdnaiQ4EM#*m+GoP;!bb~ zPbiY!+rt~44Pp2Le}jzN(KL`!H@ghww!=8Av)eR}n>?WY^yqe&Wu4srfRTxk{PD-q zQXsrnPB$HJJPXvmww6DGXZ!2xX-uH)4e&jQ{<5cW7up9u8mh9o`kTJ6O68OzgZn|U z=Lph3DBN^8W+1>8E9!S(&WV4!>rjHSNG~1|OSc|xJDyuqHuXOs8M$Bm<_kyd`5nlW zb3b=5R1v+2W{%7M%P^EJM$0xQfkt#YZZb(!iHD-~y^F>#;qR`ZuSfMLlb}@Me3A1Z zzZW2!9arg?5jUxNK#TuDS1d*s3uh4?5>?j{tH-Wt zo*<0xaolWhY>!DtNlBZ^=8c!^XY#@&lE&XYCB-#NOO@Lgg@pYYZvv6Uv6}h5lHKf2 zWCuU1j(YIa_H6%}@!8z=-blHidvPN58|?t5D~xTqp`-aeqO7u0B2A&5dg?gHXldFk zcG;*DriG0Ep#3yTW2ga4%Sz`PGGf)izyJ#PrmqKr`u(Sy_&V=W$D%*yQ%X=bSv!o)^~wg|LK)S(ZL(TDyY*=;gq|2WmMtQHB$1{@mUCk4K| zsP_o|p2{rcRUsLh{!Jh19ni&8NQ4RC$vRyxC@8A$tisM8mKlUF zGHWPG7W3H~8>?TZqc`w3aYnSfyp#(@)Du%yj&20?q$rDQ*50yoO`sj&uXnGd1qA$9 zBD3HXp%}_N3hbUpvm_Zo_M|cA7p@mXaz{$4ZBi2v7Yx*IlKISE1Jy$hclTrjBK4+A zq~K4Dyo;DO0up0MGLq6*=FMQZ*eSFN)afD4?R3)a^yUk>wCJ_J!-iRNB8XCw>o+fa zlAK|w`5bpxYvK$YxoPo9Mgn*BegC3$`-GA!`QE0H6l!L)W^l7xthqi;!$>1FFq}od zSoK8HUU&@RduOCch%+=AvyER{^9Gh{!h;ciseo;BLn8TlnwIP)*~dl2 z+#(i9Qh^n1xs@~^1t_g$0J0``=B{;Jv zLd|ch$jw-fhLI`f9rIS~(!FA{N%xOMzM(gYh)+QgvkbVM_ZN{=n4Cji#ST3w)lJ^N z*amWE(Rowq#g6x7(ALbz96%-?q?CM!{@DzU2+@n@p6S-%prHiSvQ1RBYVkIN&qv%| zl#Q68r~>HPO7KH;$S*)^EWgq1lYr2(_-aq9DPm>CO9Paymt~8!Koyd5fSdd3=js&h zc@$-*6`VhiXr4Qj?{1~k{TLgU{O=CQNOFx*!G?{n@yEqY7{*#luoZ+lL`sz?UVnBr z^oTX(+UrVX%cf#J=QX z9zK=G&LFSvLcpfIQG#MVf~TXL&4OJ`FBz*^-A|Ghd^kfbK7Eu^rO7HHDZ(NMpSmx6cv6-jgP@{6ntFQvR&_41 zlU2f)r&Z3aq7F`m%oPCONadH?;(X{E;x2ytrlxt2K{-oak;z2kt*8uO=?)#kvnsU7 zJBuxZ!GIZpHOp5`q-fqRp*Yjj@J~kE&rSsI`7YD}rkl-*L`qU~tv4n2{@`*p0DJdZZ zExBDU^RIrI50g<$FMk740@BR!T)hOLM0^4R)o-A7PpjQ0iH|6rP2$qX0Y(D#`=PkG zD`fXMq9Q?>GQ;g2@Kflkd|D4epS{ubSw|U4&xYD*C709$MJay5+1rN?Icw(o z7x!!E?ASORm}ae{K*zsWWa+&|t}bwS4K!&&uE91y6|Jk#iq1QP)%8^lb;NmL&Xu7l zTtE#5-R=TqBi<}&Bk+%S6SF>mYYfbpIri{2%~y|9DSluO`noc)k9Z9cvpd zoj7Q-Ot4EIl3s~c7(d1181Hi!?X|A8z!Uu2*1;Rp-N4G%PI7Yr;#Na8iwp$NR>ttI z3khGZ4|u*>E88+z4A!j-jp*1r%f}abimqXM$3UTIjZ>CtA<^a3nhGGy^(LC4`LaPN z!!e9uI|Rw^qLjv5@-&^9@{jg@$F@Mb{y`^~_*0iFUk+YBoWQn<{=S@(m=&%3_xA!s zcLVuMc5q0}oC=2}nu>1y>qCu=pbLzB!OyxVb;U5=WS9anpltTrg@8}f9bA=8%Xp8g z#x4C8%orNN>fWJ#s$zXpZy&uBe%%3*zH>QNA%C>qfp1;EaSDl_6Q)o0n@>VB1!-7h zjgR=3&n?7+zK68clNF5oK2`J<6*^6VEqqweIm^*s`e<0+I3~&&kFv&1HVn30`JQ$O z^|xRI1s4-D00GQA8>~VkNVlS7{5{kU3~x~`rzf7HJXZz6V$?q_$}1-&rE%(%4TTt{QJGJm?zWv$%FE!Pv{mrAW+bS64NJ$qL2!j&#z94o$kn?J4%a0Z_x&>fk<)58V6qE+;fwE|Ni-MG$k~ZlR89}xL=);= zkXF-}C5~9yU{EvKeW5+2le?O6`1zUCu|X}u`zr4XapIxP3+Q<6aGrjZ_nzFBY+fiy z+QhzYEZ$CR#h^$}Qvau~JWgT!k0}>TARdJ2I-U)@X7YF;Eg+gY5$B~xzWZ7(zPg*r z>^W|Z&n5ypA5zLZ>?Ft$O^lH2>&$nTWgfA$)cIjC3mV?>D=)6Z1E+Fiu(9)HIwNj< zdNo`4jP+1&IW(f{3U7?;M02WKOmV|6$zEP5;42XpRqb~RQy3C^w&^jl95gZp0ZpU< z(s!gFM?zjl3Y6CEyeZ;4J<2eq)eDwahiRyAQs1{n3C={YJ-Ub>S+eZ5;{Dc)ccB`a ztoUi5fzw!Br4s(L)#`IoR?&@rnnjqqTPw}bI@05M$YM>GV><-A?t%@ⅅ!brqg zj2EbA1A%FOMxUS~JHoSJM!Nfc^Q$}du4v;17qJU@BPOw@IlHVY?^k!zZchwxcbBC4 zVw8_JB6IsFZD;(Eyf8tawquirP# zr1Nji?}0m)r`b@?x8nNZuco1Koe4kM&l#m1llz&{=qxiMq;WUAKcDP=WL`~wf`W->LV~&>I@Z*BE#)(R4setkwygh{vx~Z$S#$SsOJMVg zt3nUsUQlw|ZhU&|w&tXP&xZQcEGyhk0uqRX7O%sNjb3JC)-|#Hw@b6?SAwL1Z;RX4 zk`Ak;R}zI-Yp&U=ceC&=oo}K<-9;3eHNXwY)>{mHhPpdtqVy^(>=X402aLyk;{x@T z0&JnMmcwd{_~GRlgEoJ9Lc*G$J}UH^1(so$-7kQQ=MPlBrwfI(99wZJz|CHM59D(bbTT!+PNmX_bhx3n=<+C|0k!m# zCXM0L_drYA=|*s6<(Mm3wu~Dv=2(wSUHi>~;j@_B`fs>ByiEZ1OBoo`Vor#S*ww+9zOl;e>olI=o_vZQU%YCzZw!cB2 z)7|G+T~}3|hx2S=<~J13x*rUE4wjTR2$jJ)Ymk07+c^0odS@Y!;FP?=#gGbNU-gqu zf(VawPs74ak0GEeeyoByrErUIErJ#27FjZ*$R{S7!6m^PMCJu#trklJumGs(+E)a2 zC(#i(=ZFYShT$Ofb48AOp8~YQ&s_!Ak*?&dT8^fkZs=uk5PoBqSHV5Hi34Sf<|n)V zDbby!8wj#ehWg9y-LfAC>^~DBsccKnsL2u< z2)K037Uo_Mmv9W9vfJWlmIHjg(uXxfk3HI3_djZlMo6)=!4;JA)PyLhWWdUt23yuer#`U)E!LebXvu`OYs@kl}DJKETpa6u58m*+u z_$npSD-t_~KjgvLE(E@@kV0qtTtNT)%uH>PxgkRuuxDPHaIG-pe<+GL24+yQk} ztFWIZiHGN+GANER>wqG4yquo0ziEm>B%b7eo&9%VlNw!)atD9z5DGWc5~XK;I_&E< z)m%@L1;)S88kzqmYh|x!Nx?77-hcdYRygKQpuR049i9y=gbM0wzj+q!_THf5as*Da z z{ub;Xanij9yRhW!?1^=lDQg2Wy3Ramd++fn;G#}+u%AcawjdP4VunG5ZZ~B5&+WxZ z6Z^QeH727qKIIrIhN#={E77NhY-ZCT?xQP51a}qS`z8k^p8EC7@&67- z13lrMP9j_Z35ZF?rDh1?; z`4nITL!N?$cJxWfyP&5%@OJhDDA)CbmFGkqHG!uMbukLl8~?@v)OljA4}!YOMKQLU zN${>}&T2u=+Wqcdf$t8P+0dCc;QHF)HBb{qz?h4p%%i=41>gm%9~Irbp4 zatQ0jzG93G9pX+|ImCG+uFKWnRgA)r!4oJIjHZT38t2ZDFDw{fS5?9=b*?WLKzo~u zPC0QbkQO_-uGppIL$ULQ6)km~pgNI+Vb_j3}X)b-bd1)4MQfO3-qr0~lvGYuiViq_k zGaMGu7A2WhlY&kEP71H40emO1ry?!KSOWy_?B zAkJ5oJXx2t^Bs85HR%A1IJ%ota)q}6?49N%tT^n7h-aMjjS%?NLVh-rkTAXX@gEq!%XX1)0D7F{o*-$Vg_@t4 z(L3A5L!A}s!hJ&n2WqTq*y4pIZ@zqE^;TJ1+h!w7+tH-dRO~fagF)m9Bzd(c#s_mO z<{vL6Z5!tMdv~(LGiXrY`SZ3 ztXvxK9IvdTG}5;M0~}Yk!G(eJ7pDZ$6|QyTRhr=Q8;-9JL2qo)epRUVt{67!~# zmbLLhfQ*-GMETbtO(4^n4}t9%_u{}s3Gv6Gy_KivR2hf%Jt*Hk6aS(79Oyv+C9LWj=V7p4;#YKm0QAj*gHwF5;?ZtTtoB$m zMElQePk;=#(Xo&EBkfrq5(*i8G`U_w6W?Hj86ub33$wl)HyaiGDIvL?e zm4ZZbb_|t>0%Q}mMNJ?bi?#DsL24B*Q%{Rlhl6-OMD#+?lw;A3i(hB_B}d5?4u^Yw%bXui-U)ABV;5S20#wjSDeo11)bP=;i$Jk(A%C7 zk1a0G=N@(5w?tXroAnxX46O||?XBUQyWq#tNZ9mG`|(P#AaPo2HCNPoOxO29jwe1w z?x10sv2XuS-KLFN_+44~V3!c=c_4Tp+0FhsBj3_tGX`jG#(kY(RgU<89$ZdEkhGk4 zIF-Q`0Qk57!~OwKLj6UAI#8~savl`+7DiiNlRzhBOu3G8MTckgN4xq zy1arm_zFNA9as(8KVHf2)fB_777$Y0I>bW?dam_@s|xL|46)%CkCa85#SKR6nk|p> zV-FYZf0z<@Ii}IIi;F;RGUNP2F6=Wz+o_??2FSm$q}SJjuO#=ci>+d!$V2d?n`{{~ z3~TL@T|dN?(+1+(MlW)?H#&x1P=ho?}P9wyZ@qx~XJ$2!a}C5ucesi#>Yh z2+&sFyl|qXb#!Gu`o#6oa2{^JLdY5+v5F$0RFM-2wxK$Nt5YK)p0{U6#tq@zXT%FD zv_Ng5OYq*uS**CJtV4!bV4?aV{2WbF{Z*i(il#cIu*PalW8Le~Yz6J|t9YAcKTXZM zP=EBdRxUo%BG)yQO}U5ka@F~XFf(X2Ve#K z^p6AB6iUSl?DIWww^YdpRhK^(^@yiBn}4_lif9g*5UO-eQT4y)P>D*U?qSMlgRMILcDR~E}M`X?fqgI2?rE0?> zy+ACGI4aZkAwi7!(3d%rSoB0F&cd>-o>ODM-uCVWm|g1(lzItPN_n|+RlNa5Z*hGB z?XG@baeV-llN@4+0f7Ag#s9PS;$Q<9wegwqCFst{CHD@Ts`?ZMy8ln{#jodK#MNn{ z&7B#%mQAhy@8S#P|L5k5|C{0qLV&=#ecATV<0#W#1YJkqzpb@@Y@fi3-&}7!GE!!5 zJ<7MIG}QrgbC8TG-$8wUPx#L9u5um>oHs)hBo)w=lZ5K%ic&{Mlo0*u^4W4tbZSI? zX(~sj=t%pYOH$=GRr$gw#W)J5WoZnVJwhBT75ms68vpi-ztUf~%4FSmh5#&+!3XfEN^A^#XdxRAe&wKw2d zLCgAqI)6Hfzec9z3xQ3^s-eKP@h>zvAzD%qm=QTj_2Wx^iL<;+^&2r;Ee%=aiB@Ad zA2NVs-irV;M%~yShDM=eG=6>FI5bZ7oq94~p7qhc0~8pi&IZ_-e5+!l9xPBOcM z;<8ai0@X?;$)h1+vh4@s4de|e^WY%@f(?W!_$~?{NpdY@pe;#`Ef|2Orb8y{Q{OA- z0WYBM%EZV{rD+KMZQBW)cCd3Ob}78)I&y5N74K~#))m+g1wgrc*;HYZUu8WA7aB7x zgu}PbtyI08k|AMv<*aRhoU$I^=6RKe3cXHU1VJzxb+dhJr9)uZ(1=gEAeovqgU(rD ztQf45xzx-NkUBm#H#171GN5Hd*-JtS!Q2`mwJL8`$7WGes+SU7vUeSH zk>rm_>5$wjV~9=kSs%BN52_(~{p_JXd(hbbXt5gyHm?B?KMlmcy?7@*c|awNKJHXD zg6`Xa#gn?=vuBo0tGuseByi(KF#G7%tsCR`r*~y5d|+fl4j_A&LJE-0yl z|G6L5v)U}{W?W_L)!qoHm-YrI@4oOombJF^``{b-R6FkZYw)%|P0D5XvfUj$uyg|QZg zMi7qhuOwXqBIa-W@;IbWmoM5^&f`dpg^+736YB3-1@bwAJBe)Y1+afk=1?JWjDFB| z{hTugWzGDgOi?Pt%Nne~78vKOQBJTmxTQU?A>;3lKUb@zjoP>!B{?EQo72H+m;Z-j_r5}fHN6V6e^P;lYfv>)&yAu_7S7M?0ik^ayw zwJbg%mNJVinlaTOBZG5P72)CAZI?zvPf6{Fu?~2q^=oO#t2H zfc6_Y-`0RT%{NjgaETNmE>~?&xrnRJwb}}QVH7ue5tZy99c1T(iCQxni zaZ}ujqS#X-n9oez&yyHB=`vftb|`0#?rHPkcm(z7(2ZD$kX;>hpbdpV2YCwBDE6Qa zxWJEN-h!nHz4N)H*8_C#O*8)}*}94`bpQ33B-lZvz%kSVXW*Mi7)*Nx`ws1N zsDFfA>ATk?HFZ$quiMSR>#kyNZIWXbXg}k`wG7pmnhme&q1kC>dOr-#n*ml+@xp3xY!;$wE&zkoULe^|lj1F#*y;U{HCo;AXyi}_3jj+pg zlWpvyKEf>@k}$L}A3Zc$AO+%bpLHmc;{iRoEiDp`oz1?eSQzmguz1hkA^hHRskiZE z-%|%PkS@0Z#@rxh3?}Xf-<6I1m>8z?TJ>7`8S5OlGLPz#a)%`S z=F|__kwUFJB`@#r((my#{xYdsEB}tQcvbtlsYEqY`^)=6q>HC}grGPEymt97{99A^ zA7~c=?2{XQ_4B^ zGv)jMp=(cVO!lq(JS6DAN=zv@6!#Q6VW+#QzVo(+Z#?4;W(t*ahBa^*I!4z&(BvG> z9b@R^+}2Ny0~mn$x`kv{49NJ@#kz#({biMhMj3phgi`wevR(*2wa5aAHgq<=A*^K8 zFklbT6VWVY&1aay1oxj$3Fe6J)T^!&;mvBm3%}0GffyUFVka#h%GuRjl_>r|Sq{U8 zI%zEZL?>EnrTaj;Iw{1AQR6Y=(*1gu9zx zl{F+XxzV?-Y3_1?(AyMUm|puGt*N_@4>|zujzZym2pTN+4O0sG)S>-e2S%)5%_lUZ zcgwR*!go>46tpzomFZ)`-B2^$Ei!V7{%7v}NBXReLRaS)p9?+vW;>&?W8khb5->)! z4`_G!`%>qrXCnp}_P0nGh&++@!rLnNkU0!lddRZVN*CZG}QfPzNp#emC zV6>o?bGKxY42=7HU_Y!d8qFWqjf2#? zK5{OImU4O-IRVf>@EW(tVSqh}05O-vBM^qFziSw{&#H{^X%GyO7x;C3ahdVZ)6ulH z`c2y{6#((Q4Xg$wteArU7)~}~kx|nR8tirDwj|FEWmRC_6kr+dnqhbke|FN<|XiB;GH+e~X6D_kuBL4D#~6}=q(BiT9g-v5c0 z6y8>)(*JTw48P49NzAv69-Sh;B0t!riy&(07RV}DFVp4^C~E}Py#OySztzmQpr<}DGxx~y+c3+VOB|j^?z}N6YPbq5-?QC&|Ud-q9Kj>b& zXxWExp%VO2I~xe=rCQnDXRU~hT&Emhmw2TgZvC*yu#02CE2l`zs7NoY*BzgHoy=sk zQaFghInx}oFX2e2J77p6-QF*WyAnKuB*#~F_W|6njwu8>LUcjgjUxq^m@Lwq1h8)I zO_hi8V;UL{_IWS4sr?$mGd3)O4@}Lk8FIuIYj6;&(~KO=e=`OC#>V%25kzT#g?g&L z>Z$kyT&F+x=y-R;0OS6C)4bO$3nIMG-AK>>#4ZJnIN z69BX+JmrtPDmwjoK0&JcFIzak5P;g=o1(+&p=LyfdLnDBANrQ%!@x8waMd%w)t@iH zcD|07&k<$ipC7Z9&bMxMAzR9lkWp^0}k(`L06?E+8}9kg^1_#d%+<=4^&{3g@ih*si2CNbZght%(D zEkV*I^bt`@cY)rHE_o-}vn!fV6!92c;>vllyE>?;&Q@jfUow-P0A8F)DTMJ`fO|`a z#dq`Nmfa2TKM~iX5S4dNV*mhu_Ir8h7K2yU&{+F^b7D>Is>i>AwxCBOucXr-3OvnI zh(DsN5Y#D|3dc&R=5Wnwbsh%350cqo4mNR&#WZ9%$ongtxbzQ!7C|n9%p%Xd<4=aH zQdH0L0m=(M+&HI+#7$}zr9{4R_>U_?KKk1TnCY{qaPXRBSy-H6P61>r=5(PH53hwI z$Qz(YajiYeByFl@iWuUi{kOi{1V<4?cs?I-`&qvijS>SldM9jtK$t?ZNp1x!Ys|)< zpd=T^B^nVbknk?~R1cew zdW6$qqBJ%o1fU;UczT*Y-|dgf6*|g9%C?U*p^?@Uv|-#Jq{7%3;c{g)PW<4t3Ik1L zH9dzIU7sPJS^kH0~=gRQ6sHhK6)r*!hHA*4xk`vi)iBssqL?Id1w{;S17GE zeNe7R@9L0pm|wNQ`Xq936gy*7e*P5Yv1~Cvdr(5j;KkIRJ=Dsqx36ANi#1@|=df!X zmA0iC;Mcosamht&{@lS=JhOg>cw_c zs;eb34nT=15lh;569POW%-y$+AxGk&ytetbQxAK#$5V zQKGo|Q*um9!&|!HcQXcPbjXSy(E`i>n0@pVrPrv`2fXtln9j+%PEgOfWE&O?6&oZIzs+t#%{u~qTBdy@OL zWv@1zCQA&2n3ZE!sFG%5hPH;~LctX0p5KA;TB}(>a2_ZX>I51jYa=c>xsuPgZ7N2_ zt})FoR-WZEl08`~8MFzo+}?|A-W4GdN&u!$`>ojFdXoYc9HW~!YyW zZfl)W=v_uhNE|J_hqvM{s>PqBplYcpHMIS@1P})&FCWtf9+%PiXL8nVJxIdLD=s7Wn-1=O2hASfHv^HM^Ib#fV*(b`R`aZ1RU zjYFpJL1UF(*y+9nac*S~FWkGcUdgy=63Sppoy|21gM3rBW5DK@6Z&9@38$ig{4ICZ z*g%aa(>g6GjCeB<^JD|KT^bBP*Z9{RS$X^~11;gC=+ z+6+^vTUWlFWUuI-M1^yjsUPW|p}+wMM=gJ^%V2eBkLw?Bv%HE_NqT+6_ko!9(RUQ| zV$I4VY1|RUxPqObrNOSrEp1{Kw1JrN9ONT1_VBnvpgLbe_e+ctU<9h-CoY`G|hFuc4JV0EhpV=hv9=Fd^bXbl0A|9J(g4 zGXL1U<8p`ZJGtLbv&=G1?sn{&=q9H;0@W*xJGV>J#K=sUv_2Q$;tK;;s*sp~ZTV;J zXIgwWnk(Y;r3t+ESRO(sOYVTFi>&iHYskMG+x1t~f%S)KTo5BJPj$M|#N-!(oTfp7 zV0_bz9`MA!vGXfOPmjFCOZxnw5r>Esj{*#~QUmCP}Ns1t- zzoul?Q!*n3TrL4)@lko^J1QcU+AVPM%IV-NS0WuLu2JW)dmfr_qSlEIc>)70D=zFH zQBRPK;3Rt0EC=SM86v`J?c!!iRT-{^S#r2DvoxbLQ_TJ=^sUNoi)dO$h7wZ+zh;TV z9p(a`r`iqDb1DLcP$Cv!;9^w^D%c9_%6}yhjRhrA-X{W7eKF)t6;_*d_-6Y4_KRLV z4f$WSc%G{>sjJh&P+E4$1?C=7J0o4)J5s+^w%z`dbLRO#wKs#j4^nbOx&~VBsfq!$+#h8% zm#)((tpJEeTGeuCmj>xd42&@AIJy1newR97E19l8hnyMAoBQ-r8}gqFH2&-?CL|I2 z<`J&p4+y2oqCReV8uNd{y|k?I*=W)l#ziU~`a`9gxot5uz0iBcMH=f&(=*jjFquq) z7&x`2eb;~JOCc+=sNi7nGKbU9*6i4efIu3rKQeyTF0_m43JF1GaflBb^dXESYKAgZTUa6XkZ;%}XjPyugc7b}7IQ~A?blX z>n50$;;Gy1`A$G3*-Gf*jcjI!R^$ zC692rAcr4IQIz?W<(dICMz6`K$4D6970MvepT8}C|793vaUCm_arty#$E5`z#F#Va zhRbcekYfwFpq9DZ<_$S*fx8WaK{bdO_EZLgZ*MfefWzFsKKkG@s+xxWvCr^ zH?JVfK8`Rd!K`sjxK+!;TCb4wWREqcStNrBsWGeS>g{c+l(w> zIpxT=_2i2FmNQvO`^qx4e{^dWskVJ5^Zq(A`Dt1KX zGsm5E$f8eK+RrblC2K&4{mx7Gek9VEV0OcB?Jc_W(p#%AS;)jOYH4Z*tk#cGFwYgq0!EZ>f(aFj(oL^l}00u&hXJbjD5MYFpjr z)otRS9tuh)$|JD(L|{8BNv-_M=;hD5OHkx7xm+}jj`A zojC;jQw_8y_!@%qq^$f_Ywm($w>W(Ni2O~swH5o=7^_A5NZ-PP5Q-u4Wa(-NZisXu zy$tOLdfVT^GYj&~%6629(C;9x5as8toZ$&Oc>%`6L4XbJ#EZT#Weyav(@2K{sQjt5 z0k<}de=o`$AMA1h94TQTx@g{&Tox)ArU%YwK_L`6c>T3RRToH@1)`dNeZ(?T=e1M! z-E=I*vuh|Sd)v~qL;!`#a_~OLqMu3ReyAO4Jtt%un(^g92SOpdbZirKdCG7WWxZ`X z0?30GJb*u%#c+S4h04&9GZb#}uL2qu^Ime>mD8cC4Z(d?w8Dr~(PK|=75p75@OM{) z_ehRL{u*_*98f1Ub?=rTH60d~Z&(RT ztUGE;sBfMwXKr9?$j|2G@_GW{dQfGGDwHyDWyIH;6COd+1S_xBoNCCko(#YOhK!4y zuRZu*!qy4c9ZG1`DKRe!rsI|qc=}O7|R=E1s#AmK9Fw5G4+_a~K~@tc(RAu2dxUb89TVxKTGv<^fq+ zAT3iqpud<81h=-P#J?J%+5db=L_k_P&H=#A2b1^dM^*$qXmP`oa&jX59}eqDH2lF~ z7Y=2mTEtvSOSnx%9;7*n)3tuP@VF*wdy%o8bZKaqq8%@cg_A;~FE9)}CiS0SAu3Yr zamVygc`l#bR-m6p0G|pfl+U^%c~$LCx5q`!6y$hR%47Sw(GDmf&&IP zmD#hVf?=Y1xx1KRN`lIl)s&Sa zE3M!j*l&cGVt^WyWV3<~u!MQ1#U-94;hj4_W~gMlrY>h`-pYaYZ4dp8)}=ZU`>ADf zquIT};YFGnwVX~zBxtf0$OJN7)$8wtX-Tn`B2tQ=6k>L97*Trjov4b2OHo{(Y>aU< zyLn`;cs-(+Z7JB=Ssv>;v@++T?i&(_rhyKK$;A&7QgU=Iqopke)WUI~UesTGHQBoa z&X)EgCM$dV-l05cj6rB(Vj$Ss7f^a49~r52e60?7gXGxR0R3koDU!5l#E$kckHk6D z{8U=w)T_uB7mMAcryN7MB-iu^IBNbXdjbx9u^#>xM9{X0worOMLHp+zI@H2r#RE9)-L4;747JV1$Vc&e|OB&w){{4Z&)Dx0So);+6n2mL0Y z%L{)C3vVgqyULp@_%eS(Tb%m0D!M%M_5(GRLckcp@s_l_i0zFC^lD+h_lphF?IOa> zJaG{c3&~zixsWPtSa_NXWIaSW_U9CfBfW%rU8TJz-fGbI089({ZI;zMk;H zb${oSH79+p@dI}1S`I4$pN}G!!I8HqFu}G7di^W}Kb*hEay^F7^Y2hhP!E$_VDj^c z{wEO{)~$*8%Z%w_PI5Q&H5{-fg1kM%}lES_)R*|GP3mVSN9_xw4%l7a2=(! zT*NYDY&_L1`!x4mF!m1F6|X06hNX|HW5mc&?ugSBq{V5n9b@>|v|X8sx(jJGGPD0l zBuIbce#O7>U_+ss{D$A-Y}eH;o+PCLPreG-G`i}yAT_?|RNe|Hy)ENnnA?I{>*!}% zwTjQWR1I*n8oCVU{fJJ2eO$J7njyKg|LMMrRIcaPGZ#~ z3Yi*ezxE*)n>oMSk&u)Sfk;{{aDw+jDVON&QVpH&g@wtl_m`wc{cC2qFh_Q{D~R06 z0zv_I?0d4mkwkU8>!Z0f8Ydy#aS28JxcRi;`x*!305el%+`F#%AW{Ev$z3b4mB`wH za$28A>cTo2VtAF%;T!z9IZYV?1R}(+-gNI(hIVEAKg@*Qg6aLu3nKx#X|)6Fufw@k zqWpfoXVi|F`I8?esHxE*nBKA;wDZuBVY(>cAuW#Cctn6Pbv6^3u`j3b>pG9bt`_!R z-2Rfc0Fn!YU`kxF4MpwjdGDSQ46TNHSxkl1_-Rh8ekEpGQx;};t?uD5m?N*OSgn6L zZ<~$aUF!JX<_|r*W{cbOO3ws$)FJ#vAG5$XTVbb#fW4#PPg)fWE5_Onh9%9jW^f$4 zok&7h%yRKaMNrRJMKO-ktW@hw6Q}ZaXi5?q@sC4aCsNV?- zDaUJbKn2;?2%W{CwG&fdWS|h7l-dUrzz` zBg7Bx+)}X$np%~`1+cYRXU`~(u}YUiVmLmXWM$wvyQPuEnON%_{T9jY26_W36*HaZ%svElM@& zs~&96gGXTglzCW^lgoKPO?-m23JNG0lh|9fHag;Qjkf`U-sx?As2Bp3nqsSmE(OIq z7%l|mgz7$zDa8X3?q_-*!sZEvc&9l zykLP{mHi~sMH}D(aefE_2As>I`3DtG0_$>9W8n6D+_){`PYIW#qWQ=BUx&Uc8FIT9 zIM<{Oi?&bv-h0S^Dj9HRAqgj~xpjiW`pEZ3diXBQ8CY+M7)eI6igx z^M~m2f3DeD6ufGs=2b(#96cr<*Z_-!)xSJYh500v)j#lVz}dIs^doDC@^GRwN@mD1 zUa!WUO{`D~uBjEx(jEG@)dA!B7%79w|CD`xC+PQEk)ON&nS0w3&D4wXI1A07Q-uF< zwaX7N=lWy~kT$r!H{-D)yCV#uL=NJtCB2b$EQ#CeF3`F@Ux!FAy4mT9c(@7A>6MnK z8?0-k$$FYoA8Ez#Y3Hh^eE)KI#Liddr}>R35X^Kcst9`AFfI{-aXM^QI>g@Ou&7@Y zm9W+zzA=9%{UUh*HqD`3ta7(KM(QQIJQr&cJVUYsNFFFBUz)cSC_1QhUKOhWyA<8X zLY8|uKg)D^XIr_Fu5hp3-0G_H-hVouugHaW{$0sXGj#GZ%^5=hj1S$rfv~&vtZM-A zU%&)=)O2GHYBWjTQfG|A$E;?83inF#fa?hdXG{vQk!@nBM)?m{Ua1jD)@^%t2q}Hy z6ub^208|`|!NPRYpOxw>X+e6u9`AQdH|(1tI=+*jhoh3}t*%gkox?-{lC4h1^Wo|u zdKFH`f|+-Z1J6kM`?!4`q%*-`7jts=>*2vGK_R%J$I?N{z>dT4I3=AK;g3FT#-Wb- zHz8e!2(EW&sDBr&n+i;AMUdd3Bx!_0}Ii?)O-SK-9H>@TaN}pHq zxGzj>NynROrVpbr$;=YHQIeuFSMcO-$`N`I#MX180-7`qA+d=e4 zq9S*cn1NPcR*w4n&_*<$(?a3r{#>AJ4q%AO{3G%(LMW#gwiZNJC8M>6Di!jl`%$d9 za+_~%%hQt3e?YWXq*lJ^V_?Dt1{Q;-gCrU()Ar6z{Au+`!*kPh6STm*m!{~{`I+LQKBAxaRh39ZoI+x`}=y)m}fdeY)Ew5a5U* z3{^$;4^jeCaz=8{O_IUOgOPOEK?p;Llb`w2Arjxs&%NqrVl2kJv=3xvb|IP}j~tiG z%-=-vx9JD8&B-RHhVCXWgrpG;eL{R8Y#YJ70EE~vXcUT@9pVpE++Us7eZEh4(InjT zu;M>xXHn(S^Vm5Ir1ciIjxls_8v(Fk%(Y(+D6t-HKa54MLArYKhbn_7V~<|VfdS5j zZQdt#hR*AJD7Z^i3pU){kAZNs>Wc1F@M}p=JNK1g@IqZwhxV;lJG{AAKPpo=ZM$d2@@F!F|^8pAv=hqBZt1871qV(gs2~L zWD#+QNPi%Q#tIbW7kD&V>S^W~cZOZ!d373fTesjlC9R>svnV8+AOff#rPC_^AX9od z@cQDEvcIEFsP_PEXCj^tcTkDNO^Qa??3cj&sUiF?1+j7D`BH!YY=`JTULSg8$rX7T zC4w!g{Z}0(2Qdy(I29~2zgMvuk*JUs{!=^!7)Im5izLVNHsO zS_U_C*z|>?gJIee^$WoFYUHMaK;yxfr^#0AP+jHi-3Ih_OnE2|LiIr*S3g)GFdRW72Qb#DYrP9EB05jo<&R4?aE3(RGB#4F z;%J=3#+3w^Fvw(x+{n7)bYvr7WfFb?pf zYW*L@SG+9DJC;G}05vHfD2$)kK1&WZxj{wYVMy@T61U*_M`12Cq1yE z1;A_&*w(Xuph%{0K~dp>8K8WciJ-qE?*3ldRogmq=Y_>_k1-(Hi0sBhYOouEltp$o z0cMa8PB~BviS>y-jifalund?@{*)bd3SqNrg+X}jo>7>!I0b6Ixs+8n`(#Ft64Kbs zAGCSfPTOj|L~PF_TuE=kOm{tf6}|WNll_{njKCCV&j|o9S}VwlvHhu**|EoGNa#g+ z?C=qJBl95*K?=FS3Gw+N^3xT?q~8OM0p0-C5?GHw+yLXRTrxEJuvWHwVa9D=bg%?O*iHhxvhX0kNR)( zLe354x^L}zkG)bq{@TBtuFrRqypSs+-Er5Z(qHA)&ZGoL+=+<&UcTbq>m_6hK%ZWW zb0=Qixkq~a5R?V)8D;ao#uIP;LjGVK24WO;k>j|^u_EUIfWAmykvBKLsg( zJ)^)`gydOZ@6G>e>bv8qe&7EcWS70k-m{EI9GmQsB;%-vl4M213zZQWS!Hj^%nWf5 zk*#Exz4vyGb6)rNyg$Exet+KQanAjC+~>Nk`+7dFabE}8x6r)FT1FuHl4u^bi|+ZA za+yafaa@M|<-=XwrinePC7xgU3gwjou3OxEVK|dm*zjfMk7i~-FJCr*H_ate5sG-(d_Q836B&fjz=5_4(TBKnZKKK!w9(Uo%84xgPsZ0M|j``q2xsVIjdrsl0nV$OHue>PbfJ|uqBR2z|~V3+a- znC#&65)zNmNA>X_J(K~5tQze~4ypESIy*qE9(?vIEP5gFoVIuHh=nVYX%-_~PH`N7-n+Kc4+|9hY{#)~pE!G-T10mnQ*|d154c_iQQ9r5dEC4%f zL0k`glmGck8MScKW&RNNJYatElKdT!=d69_w>ZVU1}i(|6$N#&+4ZTcn#mMq7Fn)O zBkYnk-(H&aR_n@@xi5y?pKG0VqWJQ-l!;_SY3pWowwP!5@MbA$Z&l}YgY;u&|+)4NX^Z(?d5OK{hScCsqFIbk;Y zbW;9yIsd!C&>y+R$=`L~2T@nRC&^&{#PGFlM~9%sM-{k@7jJ*TWY;xPwbanL?9ZIM z+@l0XVnpAI(4dwVvzV_-bo({W%hgWEyD!Yd+i8ZkW_Af$5XK;(Uh1>dp*2Vin?B<& z;^xhtnO0X_?oh==vMXN-)yK$wm9y}X38Jj|Xner1nOfbLlSoDDw5o8d#wybhLn<}5 z6Kd|UDvRHuH)S;a$u2$=SiLXx7sJQAp2r<5nPy$hg-y;1XgSlntZwq-+nwOok2q3v z-|OboYpcY3x$_dD@6I!jB8qn>s0W8H@v$Rn&e-MFwNO=!a9hx3h9cb+{XJ zlg~TJA;rOBJj~~jKgU^ekuhytrab=3zWB9P$IG~yb^AN{#fH77vGw*feOU z{|Nq^eg!F4zzg3)T7EZd8pTKIgg3pl8keo)ndQM>c@OhLku7H@*Md*>W$+g{cF+F} z;aASl=Zu5_Ou^WeY*0Ll#a_D%MpOTRI_M1Eg%=i4SgdmEGi8`KinG2j@E~9heSYSr z1hmnQ*T81_{7@YFpz9R=ky;pgbe0NW9^Bb3`gA$_ehq$Xy!8kk3O=4j?>EeXcPM|5 z3PbyW(7ts}H8;17lkbZxbr!ZUDhg6m^ux5`7jie76?oBHpZ1+^Qu;afFtzO#wU^!> zdU^A{KTA?+CxbF%nHzJjF+b!%(KB$vs>=Mkn-@Gr*(R;n|QRbz%mei^%x|q(b5vuKu ze#gkKW-d+I%Eo+h;bPkSwG^4bG2iawP~jeH*)lw(+myE&U5pd)7gM=h!|hotTDM6G z6}{g}n!O6bsm)ncab#vb0WdewA zjbW`_S0x_w_o>Ai$o;(Fd(mQMS5raFCiCS=(Z+@KB3Ag4(rk{c^EkR zRy!kj>2_Or`eve+#g31}>UHkx&B4-13X3BCqjMdSS_yYY_tQ4DpG1A|_?lQ7cN5B3 z8+C3n7Nz%E_0X`~x#r4I+eNNW!ft(j<7!>c^=CtiSVQE*0~MAVnFE=(iA7fRSHswg z(@Oad2R@|#8kN8LPe>ta1S6>3#jH=ftF9ZE#zX0~N|8 z3exg^{ZL}TXU1!!tuSd+%q6ACg~wZ6FqX`dtayX8+3oKi5xL(hhzYlbq;lVALv%9x z!x2X%fbS!TFWnmd7gGGDO;E<7l&@26)8!+CjA$Ln(-5&BI*B{7Js}>JTf8!t)e=+K zODU#ICn$#^9ndX-?&+fxHankaMW2mP6OX3**?`CE`MF|b_V0q6F8y?-}-<8sx(t{@ID56D8VfvG@z zA(*X>J9$#H@x4AKc;f5gbr0L1Z2I-2m(HYKar}+SnNJO;&GQ-Lxbot%E=HZDE?;!k za(IYNw3}O7LrV@pI)Hf72MJzDu>WPu)m*L}*Of@-DR zcWu>3MgEazBfpBd)^p4#R}G9n`>ed@lP&8#yJ@d9{2YzVk<+biCO4)VXz6}`ITeX@ z^KBmtmAznZ|IhK-^PdX5Mayj_R9-X-KCWXcYQH#kFtKt|DHYuIUC4L34#Qp>cLVh? zwPb(co}D}!ck)e2LUrgpB^C)W$|^3qDYiSUkK$4e)6@rBX2`T4t|RqID5CM_e*5C6 zSGTYr-%Lu_GI{PZPCl)8YHm&^_3B)nug*W*sDQKBLhpvA{a;z@NB4NA?j2^rH z5E&ekK{%BPmv`yAVJbVek*{uQxyd`rKqPfR)U!99=kV^6262)55&!eb?DAwC>8L-F zeV9#^u2li_5KlF^UrCRDgu2l+?w#wZK74ko?wcjl5u7tJ0xl3z@X`^9#jr;8CUM@g zWbTI3e)5Fp@yVw|FI$`9m9v|P{CM^+j+9cp_4sQljTs!>m=3I8z8Rs_j7*Ys)_|Wi zr7;A|mmG&~UuW$3*w8NjQB>@HOs>IU%W}eBTZhz?`G&rKPpBTYEkoz**E&pi(4KFF z{n(|GwhL|d)+TPneaTi@bQohQ4_8>vm-zVg0-Is{+sju2?|kP|>vE_P9hQu>pw-gV z{+b?sW*eg2ZWX`PViTu8yz=1}1=@Xv^q9e&!NKC%=tZFb6@FY0s|jB!623zD#liji zT%{%1&8K>)%<%nLWkL}|_Y+4uw8Srw-2RPhj+-GB8iBW8C;!dO@7~9M*_?55lp8*a z-Hmy8&JYCoPgV}6hq2fpEk~7Jei%Gmd}TwkA(+EWGkqfXo5W;g=*ALon$+Gt3Hbkf6>KgzRO2IA9RzU}`+nQ?gJ z9>ZFTF6-jk=3S#(^3iu#S6q3#5@@?|d2{vlnwOuL-)e0rZP?v;W-4qX-qzHUu6IlR z>Z@yldb6tEm>mr#+)2Hj+;_R&DIPXW(7FbXeeUiF>&ZW{|593JC~{qjqMxtaq#9Ez zxbfa1fg{uFj;zwxWK=5@F&!!7vtJ)fk*qPfMZ?zbdgmKb^jG0 z`C-~JgyvVON6d=J2#)j!%qpsi1O=Vz$9VF)CSt2HtU5HABic%!IK!uqmaz|My0(Mf zm^trE6~g^th5V$%&1OYOlvndNO;V1Z+V3wo*`BYfs2(s%5y4Zy1b0>IX>TH&F=*k zS~Rq~FS)Sh#dV~URmfd@%Ezty8hdB=SE1o<|C3RT-nm@mQ~7TYzmZTIZA{cGyYV&I zCLNEtsl3x~h&r?@Bd_0ys;hjpi2vLNwUKzfV%KaQPWg$pZV)s>{oCElmS5X2Mm9A+ zuV8Q1|1Ki&4<$0+3#QevOpfgVBc!jN94}>=ja+$YRE z%X}DiQBR|B*#m8UaKf%bz`aN7DU+#BclEQAMcX_KT}B{r?Cy0eZR>+^kFka1Xd8Qn z$-gaz$je`vNi$65*1+o$bR=wbgpve__iKHPhaoL!IKWrYEG0!PMlH*RACd`{ZCPi> zqKF-GAS4k!_dGSFUkLi?;FTh5D6(nCuO32#ASR{wzA&F4r zTPuh?O*XJ(;hk$F=FRptd{9ugE7SN5{-<(O)ROR9YaBfApqg}b!k`))&SGg_uKER? zeymwl^FMhtd)Q9lO3Y9`NP+_|4aGBBTh)BP#NzAk)%ni?olWmk%*yk7IOt-0I2EqnI^Oc$h9 zbI=P*`189AWe~RhPRrA~`5xzLA5bD{YrFZYND>*~bF%f3=d93deDI;EYZ(Oz2J^h2-h>f)94GIxNNt|O@2W; z`pN`XM*IjQ32IrqOYYywvyq+;(_E!@V1JjBaQbkTec%Tp?cGy*7x|_|^|Mep%#6!? ze2OnqE!h;r@CX( zOVaQ#HSJ&)euWk{f_%Q;!GqotnJ7Gya*?jcz-*Pdpyg`tiypxsqIGuxvrG*gY<115 z9loFlXj-P3yGinLx#Qr?A32wu_Fd~Qfq#f-HzFWZ>$Bi-t>0+ZEahmZQ8F8eJ|~+0 zp|dNA(YZE=@+TSBLn|o-l7ZT~Ah;u4erIVRZkPuNM;)@N_4N)xC3UgEKTLzSJC5a8 zAHhPx+fxTU73M_vz-n&A(%;w#a|j zsIk<$P~ow6dA0{$bJL@)bqf1u463YyH8vY;Q6BiiF|;ebZwwm8?%D#aP3Q^!e5}D1 zd^tupz;R>Xyu+_$ubp);aiq0P7B*p;N1bJH#T37sdz0PQc4qqbY_xa?rGjSz9<8Z4 z7#@tN8cs{5MUrzm+h0mOVxn(#pDv%LT_(nlrgASmj{houB+s|KY$z+WnquK%2Fn)B z+`q0}c!idq)YhT#**w4FEIaO4pmc4MI#PXbu5}RA^b8}}fs3BiXu3VL=|UHa!kKv0 zc?WMw=7mM`@LD4S;6)d@Y7|b|>$n$F>Jn-qBRyYHS?-_&eeqp+!a~K$y9a8I(9(Dh zL$LY_yrVm9zB>#3#=#!cOaw9&noL-lg-`w;KF0(*S+XJSUrjaP&zhF zCk_aI=wGbIUGVuB*#NuKczWp4HyrO))15kYXR0@yLxt)i%ye&dc_B3#EU0^OjwW1i zS@&Fn=IS!X9qfBo5_8-P*U$|U7Y#AtAuN85{u?xi`=FTj1R{J-PyZO)a(1>1=XjEq zD`Bg|PAr8YrA+tKITpXE^%e(ishWdwfz1w3Oxe>5LVuIaQkmUf>~-8D(+#zEVHHsS zK&ZcPA-b4Wy~JBkU>$!qu|TBY&g%xX3JdBkF}to`4fq6K$k>VODOvS8WN5`?{P&ly z5rCg5?ig>;KMUCjRjuNxl&2OgfQ}v{!58fnN%7YZ%xh3;UR%W>I}R@OB=TOer4xRq zeNP+nAda6t)qy&9azpWZjqfAQ4oi`9$XNJ$z$LzP_@Y(n^k;rI`;KqT9xAGpeMtoz zr_fQPHoO}vxq((Bc3#gGZOhgbeU^N`?FKPXKA;fqvQZym=j!gscDUjK4~N#ijPLgt z@+!YMd!SEwmG=F0A3l`oXx5}KPflC=RFnM2ce?Tl+65}|Z?#cvA02(XeHyNLLA-wQ zr8P~LN*xDv;d(*xM{W=#Wxvvx~~D@(#a zUerfM-*v`q{n+*GDht+@K^JqEFoyy}_6wCKZ4LTANwcqJ(KO0_dCc27lALB3(pLmA ze{!~^;_nTCxra`h)TNVa(f6?)ecNB*VmE%cnZ86Hgb=HQAgB|9BU5z|2sH#>>KgL;0+SH6~ZBrwADT ziElVEE`N0jw7P?aw?E>^&!uBK5C`2}HW!a7pVyM&qw#3=k}80XGMoEDNAmoO*s}!7 zxv!LTzh7{T@cwL2W)_4r;}=2U)84cSXzgz5p3`Sg<1G+Rs%v)jfhYFbQ70^eLvw^x zA3>~(L}OqQv1cDf^oNZ|@HXn*1Y&`rPS!+;=hR2%MI<@mLZ{&y=Lb5yw&g@fY6o1~XO zQOF8J>J=U$W;7;KbWN%sM4smdQPA=J4||wgi^Rnd5J(Sk0x%B=abb0Zi=*X%WeO~# zPFS*09U|Lz2~#m=?R!~>9Vn=Kt4)-%0Yz#%Tj+F_mM9th)$wJ zIWOE|1NlFO02EUn2_BJs?gG#Fh$fg_6GU@JuXm{;z8{%Iv1RRziLkKT;v!D^!0XLU zC33u!A9{d;d?(|r>fYg@T<)^hyFRD=B;9pg`6J&Fbca zfnDw};^sl$a3bsVb5tAo2R_E{LMrSOG$nc+o`JFVlB%&NMczkgmeH|-7NRgy+kRt35uBm>c2xrLDwg-BrUUz2taBuPg&DSdk z&OKi&mbD6zK-AY(_Cffcj+yCN(6L4q9I{M}}G`fj1wkk=I@RTqw7@{BS&h!#f8JL;+rfJRYd%{#*`VD9L zHm{GY90IR&tR!_i=;@wq9T??21J_2s;;pT8;9}hz13axd!tCrh_rw`%KQ1X?$+m>9 zaD-3gDYtib+1Gqa1FvE#uO-Mh94Eu^3CKOSE8SEMn&z^)iGQ7Yh7(Buld?`4z*zkP z7X8M|k^Dg$WnpldW~&pcd((P)y{ZO@b{rcNJ*9rEF$mKJH2%7+4_hnck#z9f5#XHf z_GD$4b?h(U0}uFtx9u9Zs6p5#+RSnM#XJ5sDJ;zDNKEr);U&>CWB)*T5}jkz^(o;& z%!Y5ET%f$VUGsVUgICNs)=k9Px$x}GGw8XGC;XctsR8hD`6uVK71NV#5fe+^*CLC& zZ}pzs=yzP;p1XHO?=ZRZ344rF!lC2mx7x+JmIKPg2Oosu;A`~BPsQ3XGKy&*@VV~X zcak3yv19`ND*}9=AvJ@HAV~Lnz@2nuA57}7nMG6d+`HAm@MK5CAE|u*Q$Kdohg*Ev z|9foM1Xmb+hUVZ$7w|~pZ3Sog70gfrZJUehQqmqyz)TysD+I&SDLVMmhmMyqagJ@q zE?0)og4erVgsk?@wCd=Kc3n1-Sp99c(*{c7SFUS@-|w&-+C7;13nQuq4JP7SaeDx*!0oHS3Q-`d1bSN8(BooaEQ} zuXCNGmSL~6M-iSZc#>m3CgYJm#^1n$l&Dkiww=e&`2cioLut8@XK}hY9dKMj-~`w_ zInS8DlQVi}+{@pbG`an<+yAl20=IK`L2!Y0Oh`e;?vB`IkgVTdjPSwOUz?^9}}{=PQTH)8`VV(-*LMhDlLsY=nwF_ z*KqM$TqVK5Vu8G7=u{7~t(dVO_$cPqLWIk>-g&zbe}QRM&||S|bwzvwbB=|*Wiy4w zR?CBh%I~hwtfku_cr3Do3>RL2(n1Mdhca^%K3G^qwuAk- zA6~Sf{yH(LDni5oi_mg_kyihHIoZ3MLwP%|GEr+PnTV*ZsEK2^w~?a8MWklFshA1k&9)Afolk>1N0T9 z3Ev$$(uu-dC}F1Z%B_WKMCKcdOMF9ce(xGehum?aB~6{6LTDm{Q#q*-g!dRzO{fum5Y@tWLEv#Gv}^sxMxEqcAXN_6{9-wtC202n z$_z(d3{JypLKr{cw7%5}I77s0Kfc5`ch050akz$D8VA4Xp8LZt7at0ep{M1}X;183 z6vcXR>N%0{J8d|%zT)j6(9KE1`Mt|+E9E1T17d}8L68~9)Rwn{8yF+wbk)bBuMNps7&h6{S znbA7+ffv$l1FKz-_(95B!H_mUPW-be@WlKN#a_qZY$sgVw}5M4FIfX}9^YKX|B>9^ zy{o4v82bQ5ez`NW8uAY^;anO)i+ilwVuky_ovTwSlsZS1J`5P1TUHrBp7_rJa!><& zo0iEv{VmOaw2m(YrD@flIAdTf=ZJ^@LKxBP6?TH=K#Jy=<2?=c#d#J}Be`&7U7@ z;LwxZp88SVAv+VbdM_k%{XyJ1NGq#-gPWzel@BCHFWm1YY8XDRJQO4nxHaafp4$S= zdtD)fjS+=!8IjIK{TaAQ{qt_2xkz9-=NiGH^o;nk7_1e&3*1M|h9O3?F5qHR6a1IW zj5Q~Xx-G-3N-unfHyLDmr?zk5QPv8L7kznU@p0U&Qq{Z7@rgXP_+AObn{)J3Y&H--}GvV8|VeQAVmhgKB@+_@a7cO~Ep7yt!CVV}0_Js)kRI5*S@l;Oc zWI_3p>H3KSh4Klq@SS|frFY;*@SGBH%$Gsz8fu$x@r6&(uM(ig1uUS+X{&hE8NxV? zqt3sWb^EGHT|6uJlVBy7A@LG#UCGHLQW*zN|HPyIhtZF*mH&Lh5z)6HXgPjeJ1BV4 zGV3))R&mJnGWSzQQ-f2{9uVDw+R0nK+U=U8l=?_vghy*0&h7zGKD76M2Wf9Mc)%5s zT}ll|LrYkPuNt-*DkeE=yR_Cwb;+DOJaG-FL_!8HUd87F0jwV9mnP;)@O-9@tbR8s zm56E$Wq$M(KcvndnnMhPQ`Kk@h6vx(x3ma1$d&pWC1BPLLxI(sMy7j9Xl<(8tKFCq z&UPvTEe)8bFyH;l8P7(qUCSkPK78NZ8DKj08Yaj%8X%_7SMGeJQh&{}#ENhXMKWPJ z#T*CX^}uz1BDYRJ{%gK{Cz}^$_~aP9j=DJtv0#vCp;YIt)O+gAq=;xab0nc?7pTZN?7~G3jy^ zo1;r(d-P=jmnM%YKQ-m?Nz-M3?P>Vgm<01a&l}D2%7jQDrwtE1%t^zaCbC^viNwUE zy?kNx)>5S<#aXQUSpV75)-*fx-1)Fg+kYgpk=>;GBWpP9pA^J-rqMs&HKQdk(8H=) zIqowQ+bnw+m3v-x!|O4z+mzGn58>2N;f!v}LEnsi-w`k88;YR_(Kg{EnAXJ<3H+QPLD6m zd<{bp!T&j`Tl=6(RQ)}$>)M6rWgKTh@o!>&Ay3*iK0ftxx~w9XT^w|;@e}L8CU9+f z{Nz41dn!@7gI18tY$%&!8LG>i0L(T^;4!KAFyIcXCSg;*0hx#rhwy$B(QoDJ832`! z$rgyC&cM_V^nM@drPoQg>Ain|aVF>t#RvSJt3JO?tC~wzmQ24U&<=)BM1g}CTI_5} zUa9y1Ao&Q2idK!e$9J0sOJ1^vbl_(R;^TBr0LAPjDPZ++B(WcYghii3yMSowV*{Dq zrD1`aWVPFQ^e=`k(yM9o90HH;P#XOTIL>HwkEL6k@%|u`L&GRq4}_p2P|rUIaT3f6 z@{=W-QKwMfBJxdv7iOT}*8;%sNsz?%z6V2FwyNDxfdRv%D7LBZHsOx*Q!NfDqgOIM zs>*$Dd_LFzemqOjStuR0ve9yq;N)fiF1xl_v)nd`LC_s<7&10q0@nXCMgts=ZfH6i ziHYn*qV`%EYe*Wm_tg;dTi3govU(jYf*}fD_O_!$^WFa&y$LOMf{=!DSpIQkSF3TQQ-CLmW|8uF|p;XVMOIu z7pz+0Uz5um+jGkIg)msObSc5#$5~H&o*<%LM?;JaV3XT^3%pa;IiHcJ zU0@+in+^`8xUh=mwwZv^LotI8J=NB0HZyA$|4W~;vW)h;dq1rIb`V?+EK7g$lqKWr zMd-_OkW$Tl0DfUw7t9Jk1LFy#W!Z^=1Z4Gas~KYGU-yx6-obx*cVES4`D@%ulRFb+ z4|{b6LKls_QFL<(2QZSP60Ivkl(NSrl8b4iJ%0fs^u9e(EC@L=?o+0P5@N zrT?GH_M@bt8%M*G4-q!=ivf>oh$WMD);~IvU4Wsc&rk1RS1+(`5xMS+vx47*DZe!T zvjTPC;mGtA!B(}om3NAa8$~T8=+QhFYP58C6`u`k5-%D@FF_Sqc=T&nrh5_4P1CQU z->DzthNxXYw?9N$8eDxF(l5w(cofl{T{eKsa2%a6)%9umYLf-@^mbfrKgXUzEQi-j z8A3GAA%1w)>^0DnGrI>w%pvb0qR1bxlMB(a0UGEVp=;x+flW+CkUsUctsX2VQJl@R zGzpkq7+Nho?*|PVJE$#CxaNsLZAKEV$Tm9|?MtYSqH37c{TnRNba4A{f%(!mQ>mq& z(A80Bd{i42IeHW(i@tVvh*x7{+JyGchU$x`K%B1QAkNg9gr?*>Cbhf7>4s7hV*l^E2M=ZHx7vkr?d?!x^Fd0H{Ob(rBE?{ zibv{PNityr1Ky=1fsV?~QW|g0p&1C+DqIhrASg)VB2asEC8uH_d(P656B&G0oRM#_ zq*tqVd#`M_b7wI8P%tSBht1}$D;IwWv)VB?t>A{wGGoyqhZZT&MtYn5tO_*d?&_*i~^caUmRy^*8QS-+KM4UG)-KKohm zgD1hC%+crC(2X$LZRMR;3%4VRW_g6ubs9hU_9VYB9owXGh+6eKm0Y7e67=cr-}P&+ zAVEElxMymBJ;YY5UtAq<-p`z!IJoEfq4x?;uc7nCHspK386^Q%4 zNXh3Bf<+>#!SAbW3S&_BneRcA`bN13a<<)cs^8M$RcCa2Hc`6h9bdZFj^VH1Jf@?d;AR`nAT-r5NK z#1eRG^nV3ilK9yUr4WdjP;5g}av6@O!RRRl+aVmc?TQ>jD!bh~SAxSnJQ51UR6|y* z+uz&oRa#%q`5oWqcDZL9FqfA*BoRZ0Ti^FOX^to7qy~q;2N*SW>Asar` zx6*Gmg0LD-r8H)R@=_GlJdm(>D1_gf4Xe zQE~pQJXz=^KAf}S^v%`pk}OY?)q)&(K9S`dG}I_F-|Fn$2|;b&@hDD8ru;y1uz;R= z`?*amD+cglY#tv1qfv16dYTd-gg)N^-U~!-Du@D#KcjrSFYF!`cR#r?@CG8K?eBTx zW6^)D5^msYaMnrv0!(?W`~n|>_SVgQ;aAm->QM9=%CHl9KE+R{J0?t=TIJ9Rwbr6h z%C*b+H1Q*ql-=4=_*9^m+7Ki4aS3E9wYIoe7H!=P>Xx#_H}}KYHvr=*AfPxxvAgf3 z3p@8|?l?~CRoA}D@;34mYp3yldFU#@M>I;Syb3s$APt|7X~6IL=DQ~z6amqT0p#)2T341|{}12jQ9CoOLXp9?(0gEDgVW28I%`oCG4}(K!SuSP@?4MF>DVqrj4W{keu+v^Y_F z=>ESRF%u%IGSGd9^pQ^hx$L5bppuVkj)`am;l=$AaCJagteSK9kT|qbag%$Ddsxxt z>o9C~JjvT_pc*LuHVI$0jzc9_9w4I*(GPQ`U?85Oe&@uS&zvaV(vC@~e7%OV*DSUq z;ncf)%Oo5cueICQ$@$bXjlZF#fwyiEkf6uMbwc-+KFNSrS$p7@4!-!I<0U-p0(wuk zs)~QY_%F%hckpWw!Rz_ocC~K=G)6hwR{x&<^+mBayoQim8*v&i>o}9)8tgqU+kV&k zCX}TYGRS~O2*^@__|GD`kxO=5&--?0%GtHY%5;&NG^B2-u@{EVeO zthk0OG(?2_!Z8nxm1VOa-!&9?BNVo)l0WHRU4l2=?mPyu1n5mcAfkv%ua8lpWOiK? zW`SbrDa2WPX?qxDF#@`o(YQL7GBtmU-pP{VP>zT++UN)QLPtoQQRp$*=S^swolFv{ zfG_m;R9|=8`_L6&H)r%d{gM1dI%KT~f86pa*yu@r%y|yx30o6MBYBOgYtrs+pdSU~ z!Hn4K|I|1P%-R!D5tn`uPhw_?RiK=5b!61lwoiPM3AR^%-lm{t=9K~8Q@$0}e?Q9( z-PXwK*kD(uJ_RQHu<31WI6@ctPeL3Tp^3dj23NK)M#R0svrjG$A}mFGe6nkIqhf1U)p6N#G?Bqn(Vhn%IuouF*PPn+=!vV?CL8OEKg%zZ7#k>1%T0Xg!0mIWle*0#KbPMcYrxI) zB;&0{&YKe+g+btJ#fl|3uM~g|^&~n6|J2#c9&|LcjlUw`1j8W7B)Q%oTBD<}t9SB! zlY$Y4=|R&xg8@SLqdo43l%QK%lc%-41uqUzkgcN6yDV_OK6;{Za(8GrYo0rh{>rml z9BqKgN1(wBqu!|}<7EDe5lFBhp2w;>klBTBcV79VqPUHDKZ6)fEvkwg5(MFJgG6KJy(BoL3s5F(*%*jksxeU1q!d>Ek%jAT1SY zk(8WtKV4oTj_^&F+Kt8{Rb}~>dwxT8{PqJcw)!3hiTfLAz#@~f`mo;k4VvGoeS{s$2JNUsNcCt=;zlx6lr7UL(kEmO}CtC{-ID`bwwH*j%iS zVAo+`Js=w>yELu;r!wh&XxFt-tEY;lz}m@b?BabuO-qOhN)XaTk#OREc0jXKYB4|J zGL))dW8Tn#^O(3j;wKy_a{4ehswta>Y6!gF&u)_bkKY`h4V5CuBsqwB1uQez+a3ul zT-XzNSd(u2QcY`$>*06(vO8VnDr!5H_d0EfytbjYMQe{EFwpgU4TDSm|Gs`~r+<44 zCg4i3&wLyIg-8kgUU6jmOnY_~6K@WowxUtgH$5ZL#$?`KoJi-_XRmn)Tt1A%4lZhlriY+!FnXTR#CI(?xVM z#Mj`Z+Op!8v~kOYSiXWrMsg!?ohV6Q#q zxk+_n^@!&M7%PBN(emTx<4#)NfG{r>wr71GXvB>kb79&7O;fS!kU{f<|1zSE&N@@v zQ(3hx0`x?9g+i4EsL{L{NDp>ISsFw$vacgYALXI1|0c-!(T_C%&>bGGFzs-Vb?kzu>3`X}ooQUg4ji_7Gwg)WJ?59S?>b0u!Ego%2HB4d%`2p&y!v>eyRy`N zpnR48(F=^-7#_M+wvpd;(0=a^y6nqfxNFG@g^^QtiQ&IkKu2*2ys!H4eNC-oklRsmKg0a z7P|#x{WjHMm$w*d&=a7%_Qg17M{Amg;nz)RvX+Y)Wim6Hino+{yZfINp6#-kqnh2H0nh2#jUFy$ zEeo@YOx@WVQ__eNaKBubAmr8>XB_hMjYI?mGFzFY*6wxLfCVXr6OOfjTkaiA+*YSAIVmF} z`|@}7nQPbR=H{i}ClwDm5gD-35M|N$Rakg3P^PW0N%lPuY^?ag*D`POsW;na{nv!o zX?cTVNCHH*NY z$8|D6=iLOlh&s@=U`Z%l#dhNEzsRdpQo6kH)6X zi6V>;X$?|hh*)B1ttp-;=%SG_ZZ&I_rh!2?4j|nhS{ftHRb<8T-J5| zI6vD_$c-%io8ms5d7^-?!Y1oNA^z|nSCgEh4Lxpg^{(``$to;a`n&Sxu6@0JbCUx7p`Q*)CxAx6@|IS z7ckxYtvh#xJ=OImhX+Z3qnLr6JkrI8inB^vD75Uy+aY(u@d5IZi185@!Pm`Mag2W| zD{k`Ozt+aU5%4o9-G{>=H?8~Is{Kt>r+;n<#xLtjsgCW4W12KL=tFnx;yK0b-dI`u zT0P9xvyg*qqgA_;o8;E>_22)Sm@|Nhv`@nQV;!dClCh3Luhj10uVJ_DxF74B!Fwau z9iINW_`XNrteR!rhvS8pc_oh1!M<-7G#XxEJb*Ixq6^tPe!&8M3o!I~W<&>Hshbmn z7YW#(|6{AN$*@=$_o0y3m9?KKBTujAqaE4STYD%x`$HNx2RoBMm4pvTDe*#Q=PG5L zP9~vdpjF`?g~iSJTWzIEF5DIpw7i~s6CagYUwz#PDrM>3e%>w znC*9g2W39#4o2nxCsbj^_v9CZ_v1$N3PBKGxP_C7JH&E*IoMz<_*MsOCg~n^lMPr17_)N zjRGQ#F(IuIXoyPt7>#|zLrA+S6edQ3vHw_o+1%E<)S$jYnZOz`SrS1?6PI|Ha$Wzz zR;XZZS2EvleaxRCGv~XPd1!~F{d{ixfLK<|Nba6&UE#uH(@N(m z+h_JH_{qg5-k2GISONNde*M?eLLEO69R`ap`D1v{zTM@jYGmoSi>acj~uu+1pyN=-wT*@|tJ# zcA?{XlPLG$(|X!@g)(Zg(@O6-OxDvHNxarW({ z-g|hA!+f)nD5S>s4p^F2O=X#P5|DV@CKS%Ugw~xCI|5U5mmE6~MoJ-nNNf&}#{r+5 zIYpB|B9<^p4=9klc@JBKQ5f+L%zs^H_4MJj>x6F?$d%bwkTL7)YN(;8FU$*SIq=cE zD7^H>(K*=H@q*kya@XXFLE0hv{&i~nfFQG~!}Q(Ml+$4|A?%@73LO7~!CE{LOKElA hSL(yo;SZ(@saP2VFWrCTr~GFCvB1#TMU+ED^nVjf#fbm_ delta 65336 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 PVIkRv8I5?k6nOs+zep+# From 6d399902caeb608884258d3446ccb20e61bc90d5 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 26 Sep 2018 07:16:24 +0800 Subject: [PATCH 015/129] fix(cms video): missing field publish_at --- lib/mastani_server_web/schema/cms/cms_types.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 716e4cf6a..a2dfe328e 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -232,6 +232,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:author, :user, resolve: dataloader(CMS, :author)) field(:source, :string) + field(:publish_at, :string) field(:link, :string) field(:original_author, :string) field(:original_author_link, :string) From 6b59ffa0f9d802f38288f05e10b2328aef699623 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 26 Sep 2018 23:24:18 +0800 Subject: [PATCH 016/129] feat(geoinfo): user info geo & community geo support both user's geo info and community geo infos --- lib/helper/PublicIpPlug.ex | 85 ++ lib/helper/radar_search.ex | 59 + lib/mastani_server/accounts/accounts.ex | 1 + .../accounts/delegates/profile.ex | 20 +- lib/mastani_server/accounts/user.ex | 1 + lib/mastani_server/cms/cms.ex | 4 + lib/mastani_server/cms/community.ex | 3 +- .../cms/delegates/community_curd.ex | 17 + .../cms/delegates/community_operation.ex | 61 +- .../statistics/delegates/geo.ex | 24 + lib/mastani_server/statistics/statistics.ex | 9 +- .../statistics/user_geo_info.ex | 28 + lib/mastani_server_web/context.ex | 3 +- .../resolvers/accounts_resolver.ex | 5 +- .../resolvers/cms_resolver.ex | 11 +- .../resolvers/statistics_resolver.ex | 4 + lib/mastani_server_web/router.ex | 1 + .../schema/account/account_types.ex | 1 + .../schema/cms/cms_queries.ex | 9 + .../schema/statistics/statistics_queries.ex | 5 + .../schema/statistics/statistics_types.ex | 2 +- .../schema/utils/common_types.ex | 12 + priv/mock/cms_community_seeds.exs | 2 +- priv/mock/cms_job_seeds.exs | 2 +- priv/mock/cms_post_seeds.exs | 2 +- priv/mock/cms_repo_seeds.exs | 2 +- priv/mock/cms_video_seeds.exs | 2 +- priv/mock/geo_seeds.exs | 3 + priv/mock/post_comments_seeds.exs | 2 +- priv/mock/user_contributes_seeds.exs | 2 +- priv/mock/user_seeds.exs | 2 +- .../20180925231814_create_geo_info.exs | 16 + ...180926065313_add_geo_info_to_community.exs | 10 + .../20180926070915_add_geo_city_to_users.exs | 11 + test/helper/orm_test.exs | 2 +- .../mastani_server/accounts/accounts_test.exs | 22 +- test/mastani_server/statistics/geo_test.exs | 51 + .../mutation/cms/cms_test.exs | 40 + .../query/accounts/account_test.exs | 1 + .../query/cms/cms_geo_test.exs | 42 + .../query/statistics/statistics_test.exs | 34 + test/support/conn_simulator.ex | 2 +- test/support/factory.ex | 17 +- test/support/geo_data.ex | 1149 +++++++++++++++++ test/support/test_tools.ex | 2 +- 45 files changed, 1750 insertions(+), 33 deletions(-) create mode 100644 lib/helper/PublicIpPlug.ex create mode 100644 lib/helper/radar_search.ex create mode 100644 lib/mastani_server/statistics/delegates/geo.ex create mode 100644 lib/mastani_server/statistics/user_geo_info.ex create mode 100644 priv/mock/geo_seeds.exs create mode 100644 priv/repo/migrations/20180925231814_create_geo_info.exs create mode 100644 priv/repo/migrations/20180926065313_add_geo_info_to_community.exs create mode 100644 priv/repo/migrations/20180926070915_add_geo_city_to_users.exs create mode 100644 test/mastani_server/statistics/geo_test.exs create mode 100644 test/mastani_server_web/query/cms/cms_geo_test.exs create mode 100644 test/support/geo_data.ex diff --git a/lib/helper/PublicIpPlug.ex b/lib/helper/PublicIpPlug.ex new file mode 100644 index 000000000..4d7824ffd --- /dev/null +++ b/lib/helper/PublicIpPlug.ex @@ -0,0 +1,85 @@ +# https://www.cogini.com/blog/getting-the-client-public-ip-address-in-phoenix/ +defmodule Helper.PublicIpPlug do + @moduledoc "Get public IP address of request from x-forwarded-for header" + @behaviour Plug + @app :mastani_server + + def init(opts), do: opts + + def call(%{assigns: %{ip: _}} = conn, _opts), do: conn + + def call(conn, _opts) do + process(conn, Plug.Conn.get_req_header(conn, "x-forwarded-for")) + end + + def process(conn, []) do + Plug.Conn.assign(conn, :ip, to_string(:inet.ntoa(get_peer_ip(conn)))) + end + + def process(conn, vals) do + if Application.get_env(@app, :trust_x_forwarded_for, false) do + ip_address = get_ip_address(conn, vals) + + # Rewrite standard remote_ip field with value from header + # See https://hexdocs.pm/plug/Plug.Conn.html + conn = %{conn | remote_ip: ip_address} + + Plug.Conn.assign(conn, :ip, to_string(:inet.ntoa(ip_address))) + else + Plug.Conn.assign(conn, :ip, to_string(:inet.ntoa(get_peer_ip(conn)))) + end + end + + defp get_ip_address(conn, vals) + defp get_ip_address(conn, []), do: get_peer_ip(conn) + + defp get_ip_address(conn, [val | _]) do + # Split into multiple values + comps = + val + |> String.split(~r{\s*,\s*}, trim: true) + # Get rid of "unknown" values + |> Enum.filter(&(&1 != "unknown")) + # Split IP from port, if any + |> Enum.map(&hd(String.split(&1, ":"))) + # Filter out blanks + |> Enum.filter(&(&1 != "")) + # Parse address into :inet.ip_address tuple + |> Enum.map(&parse_address(&1)) + # Elminate internal IP addreses, e.g. 192.168.1.1 + |> Enum.filter(&is_public_ip(&1)) + + case comps do + [] -> get_peer_ip(conn) + [comp | _] -> comp + end + end + + @spec get_peer_ip(Plug.Conn.t()) :: :inet.ip_address() + defp get_peer_ip(conn) do + {ip, _port} = conn.peer + ip + end + + @spec parse_address(String.t()) :: :inet.ip_address() + defp parse_address(ip) do + case :inet.parse_ipv4strict_address(to_charlist(ip)) do + {:ok, ip_address} -> ip_address + {:error, :einval} -> :einval + end + end + + # Whether the input is a valid, public IP address + # http://en.wikipedia.org/wiki/Private_network + @spec is_public_ip(:inet.ip_address() | atom) :: boolean + defp is_public_ip(ip_address) do + case ip_address do + {10, _, _, _} -> false + {192, 168, _, _} -> false + {172, second, _, _} when second >= 16 and second <= 31 -> false + {127, 0, 0, _} -> false + {_, _, _, _} -> true + :einval -> false + end + end +end diff --git a/lib/helper/radar_search.ex b/lib/helper/radar_search.ex new file mode 100644 index 000000000..ce5524f89 --- /dev/null +++ b/lib/helper/radar_search.ex @@ -0,0 +1,59 @@ +defmodule Helper.RadarSearch do + @moduledoc """ + find city info by ip + refer: https://lbs.amap.com/api/webservice/guide/api/ipconfig/?sug_index=0 + """ + use Tesla, only: [:get] + import Helper.Utils, only: [get_config: 2] + + @endpoint "https://restapi.amap.com/v3/ip" + @ip_service_key get_config(:radar_search, :ip_service) + @timeout_limit 5000 + + # plug(Tesla.Middleware.BaseUrl, "https://restapi.amap.com/v3/ip") + plug(Tesla.Middleware.Retry, delay: 200, max_retries: 2) + plug(Tesla.Middleware.Timeout, timeout: @timeout_limit) + plug(Tesla.Middleware.JSON) + + @doc """ + this is only match fail situation in test + """ + def locate_city(ip \\ "14.196.0.0") + + def locate_city(:fake_ip) do + {:error, "not found"} + end + + # http://ip.yqie.com/search.aspx?searchword=%E6%88%90%E9%83%BD%E5%B8%82 + def locate_city(ip) do + query = [ip: ip, key: @ip_service_key] + + with true <- Mix.env() !== :test do + case get(@endpoint, query: query) do + %{status: 200, body: body} -> + handle_result({:ok, body["city"]}) + + _ -> + {:error, "error"} + end + else + error -> + {:ok, "成都"} + # {:error, "error"} + end + end + + defp handle_result({:ok, result}) do + case result do + [] -> {:error, "not found"} + _ -> cut_tail({:ok, result}) + end + end + + defp cut_tail({:ok, result}) do + case String.last(result) == "市" do + true -> {:ok, String.trim_trailing(result, "市")} + false -> {:ok, result} + end + end +end diff --git a/lib/mastani_server/accounts/accounts.ex b/lib/mastani_server/accounts/accounts.ex index 0977481cb..a996257a9 100644 --- a/lib/mastani_server/accounts/accounts.ex +++ b/lib/mastani_server/accounts/accounts.ex @@ -15,6 +15,7 @@ defmodule MastaniServer.Accounts do # profile defdelegate update_profile(user, attrs), to: Profile defdelegate github_signin(github_user), to: Profile + defdelegate github_signin(github_user, remote_ip), to: Profile defdelegate default_subscribed_communities(filter), to: Profile defdelegate subscribed_communities(user, filter), to: Profile diff --git a/lib/mastani_server/accounts/delegates/profile.ex b/lib/mastani_server/accounts/delegates/profile.ex index d4b5c80d7..eb840f0ca 100644 --- a/lib/mastani_server/accounts/delegates/profile.ex +++ b/lib/mastani_server/accounts/delegates/profile.ex @@ -6,7 +6,7 @@ defmodule MastaniServer.Accounts.Delegate.Profile do import Helper.Utils, only: [done: 1, get_config: 2] import ShortMaps - alias Helper.{Guardian, ORM, QueryBuilder} + alias Helper.{RadarSearch, Guardian, ORM, QueryBuilder} alias MastaniServer.Accounts.{GithubUser, User} alias MastaniServer.{CMS, Repo} @@ -52,7 +52,7 @@ defmodule MastaniServer.Accounts.Delegate.Profile do step 2.2: if access_token's github_id not exsit, then signup step 3: return mastani token """ - def github_signin(github_user) do + def github_signin(github_user, remote_ip \\ "127.0.0.1") do case ORM.find_by(GithubUser, github_id: to_string(github_user["id"])) do {:ok, g_user} -> {:ok, user} = ORM.find(User, g_user.user_id) @@ -61,7 +61,7 @@ defmodule MastaniServer.Accounts.Delegate.Profile do {:error, _} -> # IO.inspect label: "register then send" - register_github_user(github_user) + register_github_user(github_user, remote_ip) end end @@ -86,7 +86,7 @@ defmodule MastaniServer.Accounts.Delegate.Profile do |> done() end - defp register_github_user(github_profile) do + defp register_github_user(github_profile, remote_ip) do Multi.new() |> Multi.run(:create_user, fn _ -> create_user(github_profile, :github) @@ -95,10 +95,18 @@ defmodule MastaniServer.Accounts.Delegate.Profile do create_profile(user, github_profile, :github) end) |> Repo.transaction() - |> register_github_result() + |> register_github_result(remote_ip) end - defp register_github_result({:ok, %{create_user: user}}), do: token_info(user) + defp register_github_result({:ok, %{create_user: user}}, remote_ip) do + # ignore error + case RadarSearch.locate_city(remote_ip) do + {:ok, city} -> update_profile(user, %{geo_city: city}) + {:error, _} -> IO.inspect("location search error") + end + + token_info(user) + end defp register_github_result({:error, :create_user, _result, _steps}), do: {:error, "Accounts create_user internal error"} diff --git a/lib/mastani_server/accounts/user.ex b/lib/mastani_server/accounts/user.ex index 5b0550236..6d24e1dad 100644 --- a/lib/mastani_server/accounts/user.ex +++ b/lib/mastani_server/accounts/user.ex @@ -34,6 +34,7 @@ defmodule MastaniServer.Accounts.User do field(:email, :string) field(:location, :string) field(:from_github, :boolean) + field(:geo_city, :string) sscial_fields() diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index 3f127b0c7..ee5729cf4 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -21,6 +21,8 @@ defmodule MastaniServer.CMS do # Community CURD: editors, thread, tag # >> editor .. defdelegate update_editor(user, community, title), to: CommunityCURD + # >> geo info .. + defdelegate community_geo_info(community), to: CommunityCURD # >> subscribers / editors defdelegate community_members(type, community, filters), to: CommunityCURD # >> category @@ -46,7 +48,9 @@ defmodule MastaniServer.CMS do defdelegate unset_thread(community, thread), to: CommunityOperation # >> subscribe / unsubscribe defdelegate subscribe_community(community, user), to: CommunityOperation + defdelegate subscribe_community(community, user, remote_ip), to: CommunityOperation defdelegate unsubscribe_community(community, user), to: CommunityOperation + defdelegate unsubscribe_community(community, user, remote_ip), to: CommunityOperation # ArticleCURD defdelegate paged_contents(queryable, filter), to: ArticleCURD diff --git a/lib/mastani_server/cms/community.ex b/lib/mastani_server/cms/community.ex index c402bd99a..2063636d0 100644 --- a/lib/mastani_server/cms/community.ex +++ b/lib/mastani_server/cms/community.ex @@ -19,7 +19,7 @@ defmodule MastaniServer.CMS.Community do @required_fields ~w(title desc user_id logo raw)a # @required_fields ~w(title desc user_id)a - @optional_fields ~w(label)a + @optional_fields ~w(label geo_info)a schema "communities" do field(:title, :string) @@ -28,6 +28,7 @@ defmodule MastaniServer.CMS.Community do # field(:category, :string) field(:label, :string) field(:raw, :string) + field(:geo_info, :map) belongs_to(:author, Accounts.User, foreign_key: :user_id) diff --git a/lib/mastani_server/cms/delegates/community_curd.ex b/lib/mastani_server/cms/delegates/community_curd.ex index c77c5fbfb..c0425290d 100644 --- a/lib/mastani_server/cms/delegates/community_curd.ex +++ b/lib/mastani_server/cms/delegates/community_curd.ex @@ -124,4 +124,21 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do Thread |> ORM.create(~m(title raw index)a) end + + @doc """ + return community geo infos + """ + def community_geo_info(%Community{id: community_id}) do + with {:ok, community} <- ORM.find(Community, community_id) do + geo_info_data = + community.geo_info + |> Map.get("data") + |> Enum.map(fn data -> + for {key, val} <- data, into: %{}, do: {String.to_atom(key), val} + end) + |> Enum.reject(&(&1.value <= 0)) + + {:ok, geo_info_data} + end + end end diff --git a/lib/mastani_server/cms/delegates/community_operation.ex b/lib/mastani_server/cms/delegates/community_operation.ex index c9989ca35..03e5fa009 100644 --- a/lib/mastani_server/cms/delegates/community_operation.ex +++ b/lib/mastani_server/cms/delegates/community_operation.ex @@ -5,7 +5,7 @@ defmodule MastaniServer.CMS.Delegate.CommunityOperation do import ShortMaps alias Ecto.Multi - alias Helper.{Certification, ORM} + alias Helper.{Certification, RadarSearch, ORM} alias MastaniServer.Accounts.User alias MastaniServer.CMS.Delegate.PassportCURD alias MastaniServer.Repo @@ -99,16 +99,71 @@ defmodule MastaniServer.CMS.Delegate.CommunityOperation do @doc """ subscribe a community. (ONLY community, post etc use watch ) """ - def subscribe_community(%Community{id: community_id}, %User{id: user_id}) do + def subscribe_community( + %Community{id: community_id}, + %User{id: user_id}, + remote_ip \\ "127.0.0.1" + ) do with {:ok, record} <- CommunitySubscriber |> ORM.create(~m(user_id community_id)a) do + update_geo_info(community_id, user_id, remote_ip, :inc) Community |> ORM.find(record.community_id) end end - def unsubscribe_community(%Community{id: community_id}, %User{id: user_id}) do + def unsubscribe_community( + %Community{id: community_id}, + %User{id: user_id}, + remote_ip \\ "127.0.0.1" + ) do with {:ok, record} <- CommunitySubscriber |> ORM.findby_delete(community_id: community_id, user_id: user_id) do + update_geo_info(community_id, user_id, remote_ip, :dec) Community |> ORM.find(record.community_id) end end + + defp update_geo_info(community_id, user_id, remote_ip, method) do + {:ok, user} = ORM.find(User, user_id) + + case get_user_geocity(user.geo_city, remote_ip) do + {:ok, user_geo_city} -> + update_community_geo(community_id, user_geo_city, method) + + {:error, _} -> + {:ok, "pass"} + end + end + + defp get_user_geocity(nil, remote_ip) do + case RadarSearch.locate_city(remote_ip) do + {:ok, city} -> {:ok, city} + {:error, _} -> {:error, "update_geo_info error"} + end + end + + defp get_user_geocity(geo_city, _remote_ip), do: {:ok, geo_city} + + defp update_community_geo(community_id, city, method) do + with {:ok, community} <- Community |> ORM.find(community_id) do + community_geo_data = community.geo_info |> Map.get("data") + + cur_city_info = community_geo_data |> Enum.find(fn g -> g["city"] == city end) + new_city_info = update_geo_value(cur_city_info, method) + + community_geo_data = + community_geo_data + |> Enum.reject(fn g -> g["city"] == city end) + |> Kernel.++([new_city_info]) + + community |> ORM.update(%{geo_info: %{data: community_geo_data}}) + end + end + + defp update_geo_value(geo_info, :inc) do + Map.merge(geo_info, %{"value" => geo_info["value"] + 1}) + end + + defp update_geo_value(geo_info, :dec) do + Map.merge(geo_info, %{"value" => max(geo_info["value"] - 1, 0)}) + end end diff --git a/lib/mastani_server/statistics/delegates/geo.ex b/lib/mastani_server/statistics/delegates/geo.ex new file mode 100644 index 000000000..7a0b9c628 --- /dev/null +++ b/lib/mastani_server/statistics/delegates/geo.ex @@ -0,0 +1,24 @@ +defmodule MastaniServer.Statistics.Delegate.Geo do + @moduledoc """ + geo info settings + """ + import Ecto.Query, warn: false + import Helper.Utils + import ShortMaps + + alias Helper.ORM + alias MastaniServer.Statistics.UserGeoInfo + + def inc_count(city) do + with {:ok, geo_info} <- UserGeoInfo |> ORM.find_by(~m(city)a) do + geo_info |> ORM.update(%{value: geo_info.value + 1}) + end + end + + def list_cities_info do + UserGeoInfo + |> where([g], g.value > 0) + |> ORM.paginater(page: 1, size: 300) + |> done() + end +end diff --git a/lib/mastani_server/statistics/statistics.ex b/lib/mastani_server/statistics/statistics.ex index 11759fd1c..455044ee6 100644 --- a/lib/mastani_server/statistics/statistics.ex +++ b/lib/mastani_server/statistics/statistics.ex @@ -5,14 +5,21 @@ defmodule MastaniServer.Statistics do alias MastaniServer.Statistics.Delegate.{ Contribute, - Throttle + Throttle, + Geo } + # contributes defdelegate make_contribute(info), to: Contribute defdelegate list_contributes(info), to: Contribute defdelegate list_contributes_digest(community), to: Contribute + # publish Throttle defdelegate log_publish_action(user), to: Throttle defdelegate load_throttle_record(user), to: Throttle defdelegate mock_throttle_attr(scope, user, opt), to: Throttle + + # geo + defdelegate inc_count(city), to: Geo + defdelegate list_cities_info(), to: Geo end diff --git a/lib/mastani_server/statistics/user_geo_info.ex b/lib/mastani_server/statistics/user_geo_info.ex new file mode 100644 index 000000000..f1febd4fa --- /dev/null +++ b/lib/mastani_server/statistics/user_geo_info.ex @@ -0,0 +1,28 @@ +defmodule MastaniServer.Statistics.UserGeoInfo do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + # alias MastaniServer.Accounts.User + + @required_fields ~w(city long lant)a + @optional_fields ~w(value)a + + @type t :: %UserGeoInfo{} + schema "geos" do + field(:city, :string) + field(:long, :float) + field(:lant, :float) + field(:value, :integer, default: 0) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%UserGeoInfo{} = user_geo_info, attrs) do + user_geo_info + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/mastani_server_web/context.ex b/lib/mastani_server_web/context.ex index 46cdd00c4..e162e8ade 100644 --- a/lib/mastani_server_web/context.ex +++ b/lib/mastani_server_web/context.ex @@ -26,9 +26,10 @@ defmodule MastaniServerWeb.Context do authorization header is sent. """ def build_context(conn) do + # IO.inspect conn.remote_ip, label: "conn" with ["Bearer " <> token] <- get_req_header(conn, "authorization"), {:ok, cur_user} <- authorize(token) do - %{cur_user: cur_user} + %{cur_user: cur_user, remote_ip: conn.remote_ip} else _ -> %{} end diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 1fdf8f88c..d8d9de326 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -35,8 +35,9 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.update_profile(%User{id: cur_user.id}, profile) end - def github_signin(_root, %{github_user: github_user}, _info) do - Accounts.github_signin(github_user) + def github_signin(_root, %{github_user: github_user}, %{remote_ip: remote_ip}) do + IO.inspect(remote_ip, label: "remote_ip") + Accounts.github_signin(github_user, remote_ip) end def list_favorite_categories(_root, %{filter: filter}, %{context: %{cur_user: cur_user}}) do diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index ee32510f0..64c2cdc84 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -147,6 +147,13 @@ defmodule MastaniServerWeb.Resolvers.CMS do CMS.community_members(:editors, %Community{id: id}, filter) end + # ####################### + # geo infos .. + # ####################### + def community_geo_info(_root, ~m(id)a, _info) do + CMS.community_geo_info(%Community{id: id}) + end + # ####################### # tags .. # ####################### @@ -182,8 +189,8 @@ defmodule MastaniServerWeb.Resolvers.CMS do # ####################### # community subscribe .. # ####################### - def subscribe_community(_root, ~m(community_id)a, %{context: %{cur_user: cur_user}}) do - CMS.subscribe_community(%Community{id: community_id}, cur_user) + def subscribe_community(_root, ~m(community_id)a, %{context: ~m(cur_user remote_ip)a}) do + CMS.subscribe_community(%Community{id: community_id}, cur_user, remote_ip) end def unsubscribe_community(_root, ~m(community_id)a, %{context: %{cur_user: cur_user}}) do diff --git a/lib/mastani_server_web/resolvers/statistics_resolver.ex b/lib/mastani_server_web/resolvers/statistics_resolver.ex index 9b1fbb3c1..974889844 100644 --- a/lib/mastani_server_web/resolvers/statistics_resolver.ex +++ b/lib/mastani_server_web/resolvers/statistics_resolver.ex @@ -25,4 +25,8 @@ defmodule MastaniServerWeb.Resolvers.Statistics do def make_contrubute(_root, %{user_id: user_id}, _info) do Statistics.make_contribute(%Accounts.User{id: user_id}) end + + def list_cities_geo_info(_root, _args, _info) do + Statistics.list_cities_info() + end end diff --git a/lib/mastani_server_web/router.ex b/lib/mastani_server_web/router.ex index 0975abab9..18669fb10 100644 --- a/lib/mastani_server_web/router.ex +++ b/lib/mastani_server_web/router.ex @@ -4,6 +4,7 @@ defmodule MastaniServerWeb.Router do use Sentry.Plug pipeline :api do + plug(Helper.PublicIpPlug) plug(:accepts, ["json"]) plug(MastaniServerWeb.Context) end diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index 880ecdcfe..a9865e0e0 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -32,6 +32,7 @@ defmodule MastaniServerWeb.Schema.Account.Types do field(:sex, :string) field(:email, :string) field(:location, :string) + field(:geo_city, :string) sscial_fields() diff --git a/lib/mastani_server_web/schema/cms/cms_queries.ex b/lib/mastani_server_web/schema/cms/cms_queries.ex index dc50484a0..2e0c72979 100644 --- a/lib/mastani_server_web/schema/cms/cms_queries.ex +++ b/lib/mastani_server_web/schema/cms/cms_queries.ex @@ -5,6 +5,7 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do use Helper.GqlSchemaSuite object :cms_queries do + @desc "spec community info" field :community, :community do # arg(:id, non_null(:id)) arg(:id, :id) @@ -39,6 +40,14 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do resolve(&R.CMS.community_editors/3) end + @desc "get community geo cities info" + field :community_geo_info, list_of(:geo_info) do + arg(:id, non_null(:id)) + arg(:raw, :id) + + resolve(&R.CMS.community_geo_info/3) + end + @desc "get all categories" field :paged_categories, :paged_categories do arg(:filter, :paged_filter) diff --git a/lib/mastani_server_web/schema/statistics/statistics_queries.ex b/lib/mastani_server_web/schema/statistics/statistics_queries.ex index 2c04d9dfd..113b4e59b 100644 --- a/lib/mastani_server_web/schema/statistics/statistics_queries.ex +++ b/lib/mastani_server_web/schema/statistics/statistics_queries.ex @@ -11,5 +11,10 @@ defmodule MastaniServerWeb.Schema.Statistics.Queries do resolve(&R.Statistics.list_contributes/3) end + + @desc "list cities geo info" + field :cities_geo_info, :paged_geo_infos do + resolve(&R.Statistics.list_cities_geo_info/3) + end end end diff --git a/lib/mastani_server_web/schema/statistics/statistics_types.ex b/lib/mastani_server_web/schema/statistics/statistics_types.ex index 651081ca0..5332f4c64 100644 --- a/lib/mastani_server_web/schema/statistics/statistics_types.ex +++ b/lib/mastani_server_web/schema/statistics/statistics_types.ex @@ -2,7 +2,7 @@ defmodule MastaniServerWeb.Schema.Statistics.Types do use Absinthe.Schema.Notation use Absinthe.Ecto, repo: MastaniServer.Repo - # import Absinthe.Resolution.Helpers + import MastaniServerWeb.Schema.Utils.Helper # alias MastaniServer.Accounts diff --git a/lib/mastani_server_web/schema/utils/common_types.ex b/lib/mastani_server_web/schema/utils/common_types.ex index 03ad99d96..7806c11ff 100644 --- a/lib/mastani_server_web/schema/utils/common_types.ex +++ b/lib/mastani_server_web/schema/utils/common_types.ex @@ -16,6 +16,18 @@ defmodule MastaniServerWeb.Schema.Utils.CommonTypes do field(:done, :boolean) end + object :geo_info do + field(:city, :string) + field(:value, :integer) + field(:long, :float) + field(:lant, :float) + end + + object :paged_geo_infos do + field(:entries, list_of(:geo_info)) + pagination_fields() + end + input_object :ids do field(:id, :id) end diff --git a/priv/mock/cms_community_seeds.exs b/priv/mock/cms_community_seeds.exs index 85d528a81..cf2bcb7da 100644 --- a/priv/mock/cms_community_seeds.exs +++ b/priv/mock/cms_community_seeds.exs @@ -1,4 +1,4 @@ -import MastaniServer.Factory +import MastaniServer.Support.Factory alias Helper.ORM alias MastaniServer.CMS diff --git a/priv/mock/cms_job_seeds.exs b/priv/mock/cms_job_seeds.exs index 8d74ff0fc..a8020b248 100644 --- a/priv/mock/cms_job_seeds.exs +++ b/priv/mock/cms_job_seeds.exs @@ -1,5 +1,5 @@ # -import MastaniServer.Factory +import MastaniServer.Support.Factory db_insert(:job) diff --git a/priv/mock/cms_post_seeds.exs b/priv/mock/cms_post_seeds.exs index 32960513f..7c2e54cb6 100644 --- a/priv/mock/cms_post_seeds.exs +++ b/priv/mock/cms_post_seeds.exs @@ -1,4 +1,4 @@ # doc .. -import MastaniServer.Factory +import MastaniServer.Support.Factory db_insert(:post) diff --git a/priv/mock/cms_repo_seeds.exs b/priv/mock/cms_repo_seeds.exs index e5b4f36c0..1f84de77e 100644 --- a/priv/mock/cms_repo_seeds.exs +++ b/priv/mock/cms_repo_seeds.exs @@ -1,3 +1,3 @@ -import MastaniServer.Factory +import MastaniServer.Support.Factory db_insert_multi(:repo, 5) diff --git a/priv/mock/cms_video_seeds.exs b/priv/mock/cms_video_seeds.exs index db3179bff..790c5a0b9 100644 --- a/priv/mock/cms_video_seeds.exs +++ b/priv/mock/cms_video_seeds.exs @@ -1,3 +1,3 @@ -import MastaniServer.Factory +import MastaniServer.Support.Factory db_insert_multi(:video, 3) diff --git a/priv/mock/geo_seeds.exs b/priv/mock/geo_seeds.exs new file mode 100644 index 000000000..91b0a6bff --- /dev/null +++ b/priv/mock/geo_seeds.exs @@ -0,0 +1,3 @@ +import MastaniServer.Support.Factory + +insert_geo_data() diff --git a/priv/mock/post_comments_seeds.exs b/priv/mock/post_comments_seeds.exs index 52c0d7d78..89d6d9743 100644 --- a/priv/mock/post_comments_seeds.exs +++ b/priv/mock/post_comments_seeds.exs @@ -1,4 +1,4 @@ -import MastaniServer.Factory +import MastaniServer.Support.Factory alias MastaniServer.{CMS, Accounts} diff --git a/priv/mock/user_contributes_seeds.exs b/priv/mock/user_contributes_seeds.exs index 60e40f189..9de0ee473 100644 --- a/priv/mock/user_contributes_seeds.exs +++ b/priv/mock/user_contributes_seeds.exs @@ -1,6 +1,6 @@ # use /test/support/populater -import MastaniServer.Factory +import MastaniServer.Support.Factory default_user = %{ username: "mydearxym", diff --git a/priv/mock/user_seeds.exs b/priv/mock/user_seeds.exs index a8aee831b..0c4669d78 100644 --- a/priv/mock/user_seeds.exs +++ b/priv/mock/user_seeds.exs @@ -1,6 +1,6 @@ # use /test/support/populater -import MastaniServer.Factory +import MastaniServer.Support.Factory default_user = %{ username: "mydearxym", diff --git a/priv/repo/migrations/20180925231814_create_geo_info.exs b/priv/repo/migrations/20180925231814_create_geo_info.exs new file mode 100644 index 000000000..bad498186 --- /dev/null +++ b/priv/repo/migrations/20180925231814_create_geo_info.exs @@ -0,0 +1,16 @@ +defmodule MastaniServer.Repo.Migrations.CreateGeoInfo do + use Ecto.Migration + + def change do + create table(:geos) do + add(:city, :string) + add(:long, :float) + add(:lant, :float) + add(:value, :integer, default: 0) + + timestamps() + end + + create(unique_index(:geos, [:city])) + end +end diff --git a/priv/repo/migrations/20180926065313_add_geo_info_to_community.exs b/priv/repo/migrations/20180926065313_add_geo_info_to_community.exs new file mode 100644 index 000000000..7eccb2b83 --- /dev/null +++ b/priv/repo/migrations/20180926065313_add_geo_info_to_community.exs @@ -0,0 +1,10 @@ +defmodule MastaniServer.Repo.Migrations.AddGeoInfoToCommunity do + use Ecto.Migration + alias MastaniServer.Support.GeoData + + def change do + alter table(:communities) do + add(:geo_info, :map, default: %{data: GeoData.all()}) + end + end +end diff --git a/priv/repo/migrations/20180926070915_add_geo_city_to_users.exs b/priv/repo/migrations/20180926070915_add_geo_city_to_users.exs new file mode 100644 index 000000000..0577d2af0 --- /dev/null +++ b/priv/repo/migrations/20180926070915_add_geo_city_to_users.exs @@ -0,0 +1,11 @@ +defmodule MastaniServer.Repo.Migrations.AddGeoCityToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add(:geo_city, :string) + end + + create(index(:users, [:geo_city])) + end +end diff --git a/test/helper/orm_test.exs b/test/helper/orm_test.exs index d2cc34480..fd84b84af 100644 --- a/test/helper/orm_test.exs +++ b/test/helper/orm_test.exs @@ -2,7 +2,7 @@ defmodule MastaniServer.Test.Helper.ORMTest do use MastaniServerWeb.ConnCase, async: true # TODO import Service.Utils move both helper and github - import MastaniServer.Factory + import MastaniServer.Support.Factory alias MastaniServer.CMS.{Post, Author} alias MastaniServer.Accounts.User diff --git a/test/mastani_server/accounts/accounts_test.exs b/test/mastani_server/accounts/accounts_test.exs index 40d8600f0..24b027397 100644 --- a/test/mastani_server/accounts/accounts_test.exs +++ b/test/mastani_server/accounts/accounts_test.exs @@ -1,12 +1,10 @@ defmodule MastaniServer.Test.Accounts do use MastaniServer.TestTools - # TODO import Service.Utils move both helper and github import Helper.Utils - alias MastaniServer.Accounts - alias Helper.{Guardian, ORM} + alias MastaniServer.Accounts # @valid_user mock_attrs(:user) @valid_github_profile mock_attrs(:github_profile) |> map_key_stringify @@ -122,12 +120,28 @@ defmodule MastaniServer.Test.Accounts do assert g_user.node_id == @valid_github_profile["node_id"] end - test "exsit github user should not be created twice" do + test "exsit github user created twice fails" do assert ORM.count(GithubUser) == 0 {:ok, _} = Accounts.github_signin(@valid_github_profile) assert ORM.count(GithubUser) == 1 {:ok, _} = Accounts.github_signin(@valid_github_profile) assert ORM.count(GithubUser) == 1 end + + @tag :wip + test "github signin user should be locate geo city info" do + {:ok, guser} = Accounts.github_signin(@valid_github_profile) + {:ok, user} = ORM.find(User, guser.user.id) + + assert user.geo_city !== nil + end + + @tag :wip + test "github signin user from invalid ip locate geo city fails" do + {:ok, guser} = Accounts.github_signin(@valid_github_profile, :fake_ip) + {:ok, user} = ORM.find(User, guser.user.id) + + assert user.geo_city == nil + end end end diff --git a/test/mastani_server/statistics/geo_test.exs b/test/mastani_server/statistics/geo_test.exs new file mode 100644 index 000000000..827e16cf2 --- /dev/null +++ b/test/mastani_server/statistics/geo_test.exs @@ -0,0 +1,51 @@ +defmodule MastaniServer.Test.Statistics.Geo do + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.{Statistics} + + setup do + insert_geo_data() + + # {:ok, ~m(user_conn guest_conn community)a} + end + + describe "[statistics geo inc] " do + @tag :wip + test "geo data can be inc by city" do + {:ok, _} = Statistics.UserGeoInfo |> ORM.find_by(%{city: "成都"}) + + {:ok, _} = Statistics.inc_count("成都") + + {:ok, updated} = Statistics.UserGeoInfo |> ORM.find_by(%{city: "成都"}) + assert updated.value == 1 + {:ok, _} = Statistics.inc_count("成都") + + {:ok, updated} = Statistics.UserGeoInfo |> ORM.find_by(%{city: "成都"}) + assert updated.value == 2 + end + + @tag :wip + test "inc with invalid city fails" do + assert {:error, _} = Statistics.inc_count("not_exsit") + end + end + + describe "[statistics geo get] " do + @tag :wip + test "can get geo citis info" do + {:ok, infos} = Statistics.list_cities_info() + assert infos.total_count == 0 + + {:ok, _} = Statistics.inc_count("成都") + {:ok, _} = Statistics.inc_count("成都") + {:ok, _} = Statistics.inc_count("广州") + + {:ok, infos} = Statistics.list_cities_info() + + assert infos.total_count == 2 + assert infos |> Enum.any?(&(&1.city == "成都")) + assert infos |> Enum.any?(&(&1.city == "广州")) + end + end +end diff --git a/test/mastani_server_web/mutation/cms/cms_test.exs b/test/mastani_server_web/mutation/cms/cms_test.exs index eb3598087..cc5b2c62a 100644 --- a/test/mastani_server_web/mutation/cms/cms_test.exs +++ b/test/mastani_server_web/mutation/cms/cms_test.exs @@ -636,6 +636,7 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do } } """ + @tag :wip test "login user can subscribe community", ~m(user community)a do login_conn = simu_conn(:user, user) @@ -659,6 +660,20 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do assert guest_conn |> mutation_get_error?(@subscribe_query, variables, ecode(:account_login)) end + @tag :wip + test "subscribed community should inc it's own geo info", ~m(user community)a do + login_conn = simu_conn(:user, user) + + variables = %{communityId: community.id} + _created = login_conn |> mutation_result(@subscribe_query, variables, "subscribeCommunity") + {:ok, community} = Community |> ORM.find(community.id) + + geo_info_data = community.geo_info |> Map.get("data") + update_geo_city = geo_info_data |> Enum.find(fn g -> g["city"] == "成都" end) + + assert update_geo_city["value"] == 1 + end + @unsubscribe_query """ mutation($communityId: ID!){ unsubscribeCommunity(communityId: $communityId) { @@ -708,6 +723,31 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do assert guest_conn |> mutation_get_error?(@unsubscribe_query, variables, ecode(:account_login)) end + + @tag :wip + test "unsubscribed community should dec it's own geo info", ~m(user community)a do + login_conn = simu_conn(:user, user) + + variables = %{communityId: community.id} + _created = login_conn |> mutation_result(@subscribe_query, variables, "subscribeCommunity") + {:ok, community} = Community |> ORM.find(community.id) + + geo_info_data = community.geo_info |> Map.get("data") + update_geo_city = geo_info_data |> Enum.find(fn g -> g["city"] == "成都" end) + + assert update_geo_city["value"] == 1 + + variables = %{communityId: community.id} + + login_conn |> mutation_result(@unsubscribe_query, variables, "unsubscribeCommunity") + + {:ok, community} = Community |> ORM.find(community.id) + + geo_info_data = community.geo_info |> Map.get("data") + update_geo_city = geo_info_data |> Enum.find(fn g -> g["city"] == "成都" end) + + assert update_geo_city["value"] == 0 + end end describe "[passport]" do diff --git a/test/mastani_server_web/query/accounts/account_test.exs b/test/mastani_server_web/query/accounts/account_test.exs index c01dd5f19..d8ea1194b 100644 --- a/test/mastani_server_web/query/accounts/account_test.exs +++ b/test/mastani_server_web/query/accounts/account_test.exs @@ -24,6 +24,7 @@ defmodule MastaniServer.Test.Query.Account.Basic do } } """ + @tag :wip test "guest user should get false sessionState", ~m(guest_conn)a do results = guest_conn |> query_result(@query, %{}, "sessionState") assert results["isValid"] == false diff --git a/test/mastani_server_web/query/cms/cms_geo_test.exs b/test/mastani_server_web/query/cms/cms_geo_test.exs new file mode 100644 index 000000000..2d7cdec16 --- /dev/null +++ b/test/mastani_server_web/query/cms/cms_geo_test.exs @@ -0,0 +1,42 @@ +defmodule MastaniServer.Test.Query.CMS.GEO do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + guest_conn = simu_conn(:guest) + {:ok, community} = db_insert(:community) + {:ok, user} = db_insert(:user) + + {:ok, ~m(guest_conn community user)a} + end + + @query """ + query($id: ID) { + communityGeoInfo(id: $id) { + city + long + lant + value + } + } + """ + @tag :wip + test "empty community should get empty geo info", ~m(guest_conn community)a do + variables = %{id: community.id} + results = guest_conn |> query_result(@query, variables, "communityGeoInfo") + + assert results == [] + end + + @tag :wip + test "community should get geo info after subscribe", ~m(guest_conn community user)a do + {:ok, _record} = CMS.subscribe_community(community, user) + + variables = %{id: community.id} + results = guest_conn |> query_result(@query, variables, "communityGeoInfo") + + assert results |> List.first() |> Map.get("value") == 1 + assert results |> List.first() |> Map.get("city") == "成都" + end +end diff --git a/test/mastani_server_web/query/statistics/statistics_test.exs b/test/mastani_server_web/query/statistics/statistics_test.exs index 2b199e04a..a1c2141f6 100644 --- a/test/mastani_server_web/query/statistics/statistics_test.exs +++ b/test/mastani_server_web/query/statistics/statistics_test.exs @@ -5,6 +5,8 @@ defmodule MastaniServer.Test.Query.Statistics do alias MastaniServer.Statistics setup do + insert_geo_data() + {:ok, user} = db_insert(:user) guest_conn = simu_conn(:guest) @@ -30,4 +32,36 @@ defmodule MastaniServer.Test.Query.Statistics do assert ["count", "date"] == results |> List.first() |> Map.keys() end end + + @query """ + query { + citiesGeoInfo { + entries { + city + value + long + lant + } + totalCount + } + } + """ + describe "[statistics geo info]" do + @tag :wip + test "should get cities geo infos", ~m(guest_conn)a do + result = guest_conn |> query_result(@query, %{}, "citiesGeoInfo") + assert result["entries"] == [] + assert result["totalCount"] == 0 + + {:ok, _} = Statistics.inc_count("成都") + {:ok, _} = Statistics.inc_count("成都") + {:ok, _} = Statistics.inc_count("广州") + + result = guest_conn |> query_result(@query, %{}, "citiesGeoInfo") + assert result["totalCount"] == 2 + + assert result["entries"] |> Enum.any?(&(&1["city"] == "成都")) + assert result["entries"] |> Enum.any?(&(&1["city"] == "广州")) + end + end end diff --git a/test/support/conn_simulator.ex b/test/support/conn_simulator.ex index aab260d58..73b68f431 100644 --- a/test/support/conn_simulator.ex +++ b/test/support/conn_simulator.ex @@ -2,7 +2,7 @@ defmodule MastaniServer.Test.ConnSimulator do @moduledoc """ mock user_conn, owner_conn, guest_conn """ - import MastaniServer.Factory + import MastaniServer.Support.Factory import Phoenix.ConnTest, only: [build_conn: 0] import Plug.Conn, only: [put_req_header: 3] diff --git a/test/support/factory.ex b/test/support/factory.ex index 2713a3aba..d20c5964d 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -1,4 +1,4 @@ -defmodule MastaniServer.Factory do +defmodule MastaniServer.Support.Factory do @moduledoc """ This module defines the mock data/func to be used by tests that require insert some mock data to db. @@ -320,4 +320,19 @@ defmodule MastaniServer.Factory do {:ok, _} = Delivery.notify_someone(u, user, info) end) end + + alias MastaniServer.Statistics.UserGeoInfo + alias MastaniServer.Support.GeoData + + alias Helper.ORM + + def insert_geo_data do + # IO.inspect GeoData.all, label: "hello" + GeoData.all() + |> Enum.each(fn info -> + # IO.inspect info, label: "inserting" + # UserGeoInfo |> Repo.insert(info) + UserGeoInfo |> ORM.create(info) + end) + end end diff --git a/test/support/geo_data.ex b/test/support/geo_data.ex new file mode 100644 index 000000000..12ebc7d26 --- /dev/null +++ b/test/support/geo_data.ex @@ -0,0 +1,1149 @@ +defmodule MastaniServer.Support.GeoData do + @moduledoc """ + return geo data + """ + def all do + [ + %{ + city: "海门", + long: 121.15, + lant: 31.89, + value: 0 + }, + %{ + city: "鄂尔多斯", + long: 109.781327, + lant: 39.608266, + value: 0 + }, + %{ + city: "招远", + long: 120.38, + lant: 37.35, + value: 0 + }, + %{ + city: "舟山", + long: 122.207216, + lant: 29.985295, + value: 0 + }, + %{ + city: "齐齐哈尔", + long: 123.97, + lant: 47.33, + value: 0 + }, + %{ + city: "盐城", + long: 120.13, + lant: 33.38, + value: 0 + }, + %{ + city: "赤峰", + long: 118.87, + lant: 42.28, + value: 0 + }, + %{ + city: "青岛", + long: 120.33, + lant: 36.07, + value: 0 + }, + %{ + city: "乳山", + long: 121.52, + lant: 36.89, + value: 0 + }, + %{ + city: "金昌", + long: 102.188043, + lant: 38.520089, + value: 0 + }, + %{ + city: "泉州", + long: 118.58, + lant: 24.93, + value: 0 + }, + %{ + city: "莱西", + long: 120.53, + lant: 36.86, + value: 0 + }, + %{ + city: "日照", + long: 119.46, + lant: 35.42, + value: 0 + }, + %{ + city: "胶南", + long: 119.97, + lant: 35.88, + value: 0 + }, + %{ + city: "南通", + long: 121.05, + lant: 32.08, + value: 0 + }, + %{ + city: "拉萨", + long: 91.11, + lant: 29.97, + value: 0 + }, + %{ + city: "云浮", + long: 112.02, + lant: 22.93, + value: 0 + }, + %{ + city: "梅州", + long: 116.1, + lant: 24.55, + value: 0 + }, + %{ + city: "文登", + long: 122.05, + lant: 37.2, + value: 0 + }, + %{ + city: "上海", + long: 121.48, + lant: 31.22, + value: 0 + }, + %{ + city: "攀枝花", + long: 101.718637, + lant: 26.582347, + value: 0 + }, + %{ + city: "威海", + long: 122.1, + lant: 37.5, + value: 0 + }, + %{ + city: "承德", + long: 117.93, + lant: 40.97, + value: 0 + }, + %{ + city: "厦门", + long: 118.1, + lant: 24.46, + value: 0 + }, + %{ + city: "汕尾", + long: 115.375279, + lant: 22.786211, + value: 0 + }, + %{ + city: "潮州", + long: 116.63, + lant: 23.68, + value: 0 + }, + %{ + city: "丹东", + long: 124.37, + lant: 40.13, + value: 0 + }, + %{ + city: "太仓", + long: 121.1, + lant: 31.45, + value: 0 + }, + %{ + city: "曲靖", + long: 103.79, + lant: 25.51, + value: 0 + }, + %{ + city: "烟台", + long: 121.39, + lant: 37.52, + value: 0 + }, + %{ + city: "福州", + long: 119.3, + lant: 26.08, + value: 0 + }, + %{ + city: "瓦房店", + long: 121.979603, + lant: 39.627114, + value: 0 + }, + %{ + city: "即墨", + long: 120.45, + lant: 36.38, + value: 0 + }, + %{ + city: "抚顺", + long: 123.97, + lant: 41.97, + value: 0 + }, + %{ + city: "玉溪", + long: 102.52, + lant: 24.35, + value: 0 + }, + %{ + city: "张家口", + long: 114.87, + lant: 40.82, + value: 0 + }, + %{ + city: "阳泉", + long: 113.57, + lant: 37.85, + value: 0 + }, + %{ + city: "莱州", + long: 119.942327, + lant: 37.177017, + value: 0 + }, + %{ + city: "湖州", + long: 120.1, + lant: 30.86, + value: 0 + }, + %{ + city: "汕头", + long: 116.69, + lant: 23.39, + value: 0 + }, + %{ + city: "昆山", + long: 120.95, + lant: 31.39, + value: 0 + }, + %{ + city: "宁波", + long: 121.56, + lant: 29.86, + value: 0 + }, + %{ + city: "湛江", + long: 110.359377, + lant: 21.270708, + value: 0 + }, + %{ + city: "揭阳", + long: 116.35, + lant: 23.55, + value: 0 + }, + %{ + city: "荣成", + long: 122.41, + lant: 37.16, + value: 0 + }, + %{ + city: "连云港", + long: 119.16, + lant: 34.59, + value: 0 + }, + %{ + city: "葫芦岛", + long: 120.836932, + lant: 40.711052, + value: 0 + }, + %{ + city: "常熟", + long: 120.74, + lant: 31.64, + value: 0 + }, + %{ + city: "东莞", + long: 113.75, + lant: 23.04, + value: 0 + }, + %{ + city: "河源", + long: 114.68, + lant: 23.73, + value: 0 + }, + %{ + city: "淮安", + long: 119.15, + lant: 33.5, + value: 0 + }, + %{ + city: "泰州", + long: 119.9, + lant: 32.49, + value: 0 + }, + %{ + city: "南宁", + long: 108.33, + lant: 22.84, + value: 0 + }, + %{ + city: "营口", + long: 122.18, + lant: 40.65, + value: 0 + }, + %{ + city: "惠州", + long: 114.4, + lant: 23.09, + value: 0 + }, + %{ + city: "江阴", + long: 120.26, + lant: 31.91, + value: 0 + }, + %{ + city: "蓬莱", + long: 120.75, + lant: 37.8, + value: 0 + }, + %{ + city: "韶关", + long: 113.62, + lant: 24.84, + value: 0 + }, + %{ + city: "嘉峪关", + long: 98.289152, + lant: 39.77313, + value: 0 + }, + %{ + city: "广州", + long: 113.23, + lant: 23.16, + value: 0 + }, + %{ + city: "延安", + long: 109.47, + lant: 36.6, + value: 0 + }, + %{ + city: "太原", + long: 112.53, + lant: 37.87, + value: 0 + }, + %{ + city: "清远", + long: 113.01, + lant: 23.7, + value: 0 + }, + %{ + city: "中山", + long: 113.38, + lant: 22.52, + value: 0 + }, + %{ + city: "昆明", + long: 102.73, + lant: 25.04, + value: 0 + }, + %{ + city: "寿光", + long: 118.73, + lant: 36.86, + value: 0 + }, + %{ + city: "盘锦", + long: 122.070714, + lant: 41.119997, + value: 0 + }, + %{ + city: "长治", + long: 113.08, + lant: 36.18, + value: 0 + }, + %{ + city: "深圳", + long: 114.07, + lant: 22.62, + value: 0 + }, + %{ + city: "珠海", + long: 113.52, + lant: 22.3, + value: 0 + }, + %{ + city: "宿迁", + long: 118.3, + lant: 33.96, + value: 0 + }, + %{ + city: "咸阳", + long: 108.72, + lant: 34.36, + value: 0 + }, + %{ + city: "铜川", + long: 109.11, + lant: 35.09, + value: 0 + }, + %{ + city: "平度", + long: 119.97, + lant: 36.77, + value: 0 + }, + %{ + city: "佛山", + long: 113.11, + lant: 23.05, + value: 0 + }, + %{ + city: "海口", + long: 110.35, + lant: 20.02, + value: 0 + }, + %{ + city: "江门", + long: 113.06, + lant: 22.61, + value: 0 + }, + %{ + city: "章丘", + long: 117.53, + lant: 36.72, + value: 0 + }, + %{ + city: "肇庆", + long: 112.44, + lant: 23.05, + value: 0 + }, + %{ + city: "大连", + long: 121.62, + lant: 38.92, + value: 0 + }, + %{ + city: "临汾", + long: 111.5, + lant: 36.08, + value: 0 + }, + %{ + city: "吴江", + long: 120.63, + lant: 31.16, + value: 0 + }, + %{ + city: "石嘴山", + long: 106.39, + lant: 39.04, + value: 0 + }, + %{ + city: "沈阳", + long: 123.38, + lant: 41.8, + value: 0 + }, + %{ + city: "苏州", + long: 120.62, + lant: 31.32, + value: 0 + }, + %{ + city: "茂名", + long: 110.88, + lant: 21.68, + value: 0 + }, + %{ + city: "嘉兴", + long: 120.76, + lant: 30.77, + value: 0 + }, + %{ + city: "长春", + long: 125.35, + lant: 43.88, + value: 0 + }, + %{ + city: "胶州", + long: 120.03336, + lant: 36.264622, + value: 0 + }, + %{ + city: "银川", + long: 106.27, + lant: 38.47, + value: 0 + }, + %{ + city: "张家港", + long: 120.555821, + lant: 31.875428, + value: 0 + }, + %{ + city: "三门峡", + long: 111.19, + lant: 34.76, + value: 0 + }, + %{ + city: "锦州", + long: 121.15, + lant: 41.13, + value: 0 + }, + %{ + city: "南昌", + long: 115.89, + lant: 28.68, + value: 0 + }, + %{ + city: "柳州", + long: 109.4, + lant: 24.33, + value: 0 + }, + %{ + city: "三亚", + long: 109.511909, + lant: 18.252847, + value: 0 + }, + %{ + city: "自贡", + long: 104.778442, + lant: 29.33903, + value: 0 + }, + %{ + city: "吉林", + long: 126.57, + lant: 43.87, + value: 0 + }, + %{ + city: "阳江", + long: 111.95, + lant: 21.85, + value: 0 + }, + %{ + city: "泸州", + long: 105.39, + lant: 28.91, + value: 0 + }, + %{ + city: "西宁", + long: 101.74, + lant: 36.56, + value: 0 + }, + %{ + city: "宜宾", + long: 104.56, + lant: 29.77, + value: 0 + }, + %{ + city: "呼和浩特", + long: 111.65, + lant: 40.82, + value: 0 + }, + %{ + city: "成都", + long: 104.06, + lant: 30.67, + value: 0 + }, + %{ + city: "大同", + long: 113.3, + lant: 40.12, + value: 0 + }, + %{ + city: "镇江", + long: 119.44, + lant: 32.2, + value: 0 + }, + %{ + city: "桂林", + long: 110.28, + lant: 25.29, + value: 0 + }, + %{ + city: "张家界", + long: 110.479191, + lant: 29.117096, + value: 0 + }, + %{ + city: "宜兴", + long: 119.82, + lant: 31.36, + value: 0 + }, + %{ + city: "北海", + long: 109.12, + lant: 21.49, + value: 0 + }, + %{ + city: "西安", + long: 108.95, + lant: 34.27, + value: 0 + }, + %{ + city: "金坛", + long: 119.56, + lant: 31.74, + value: 0 + }, + %{ + city: "东营", + long: 118.49, + lant: 37.46, + value: 0 + }, + %{ + city: "牡丹江", + long: 129.58, + lant: 44.6, + value: 0 + }, + %{ + city: "遵义", + long: 106.9, + lant: 27.7, + value: 0 + }, + %{ + city: "绍兴", + long: 120.58, + lant: 30.01, + value: 0 + }, + %{ + city: "扬州", + long: 119.42, + lant: 32.39, + value: 0 + }, + %{ + city: "常州", + long: 119.95, + lant: 31.79, + value: 0 + }, + %{ + city: "潍坊", + long: 119.1, + lant: 36.62, + value: 0 + }, + %{ + city: "重庆", + long: 106.54, + lant: 29.59, + value: 0 + }, + %{ + city: "台州", + long: 121.420757, + lant: 28.656386, + value: 0 + }, + %{ + city: "南京", + long: 118.78, + lant: 32.04, + value: 0 + }, + %{ + city: "滨州", + long: 118.03, + lant: 37.36, + value: 0 + }, + %{ + city: "贵阳", + long: 106.71, + lant: 26.57, + value: 0 + }, + %{ + city: "无锡", + long: 120.29, + lant: 31.59, + value: 0 + }, + %{ + city: "本溪", + long: 123.73, + lant: 41.3, + value: 0 + }, + %{ + city: "克拉玛依", + long: 84.77, + lant: 45.59, + value: 0 + }, + %{ + city: "渭南", + long: 109.5, + lant: 34.52, + value: 0 + }, + %{ + city: "马鞍山", + long: 118.48, + lant: 31.56, + value: 0 + }, + %{ + city: "宝鸡", + long: 107.15, + lant: 34.38, + value: 0 + }, + %{ + city: "焦作", + long: 113.21, + lant: 35.24, + value: 0 + }, + %{ + city: "句容", + long: 119.16, + lant: 31.95, + value: 0 + }, + %{ + city: "北京", + long: 116.46, + lant: 39.92, + value: 0 + }, + %{ + city: "徐州", + long: 117.2, + lant: 34.26, + value: 0 + }, + %{ + city: "衡水", + long: 115.72, + lant: 37.72, + value: 0 + }, + %{ + city: "包头", + long: 110, + lant: 40.58, + value: 0 + }, + %{ + city: "绵阳", + long: 104.73, + lant: 31.48, + value: 0 + }, + %{ + city: "乌鲁木齐", + long: 87.68, + lant: 43.77, + value: 0 + }, + %{ + city: "枣庄", + long: 117.57, + lant: 34.86, + value: 0 + }, + %{ + city: "杭州", + long: 120.19, + lant: 30.26, + value: 0 + }, + %{ + city: "淄博", + long: 118.05, + lant: 36.78, + value: 0 + }, + %{ + city: "鞍山", + long: 122.85, + lant: 41.12, + value: 0 + }, + %{ + city: "溧阳", + long: 119.48, + lant: 31.43, + value: 0 + }, + %{ + city: "库尔勒", + long: 86.06, + lant: 41.68, + value: 0 + }, + %{ + city: "安阳", + long: 114.35, + lant: 36.1, + value: 0 + }, + %{ + city: "开封", + long: 114.35, + lant: 34.79, + value: 0 + }, + %{ + city: "济南", + long: 117, + lant: 36.65, + value: 0 + }, + %{ + city: "德阳", + long: 104.37, + lant: 31.13, + value: 0 + }, + %{ + city: "温州", + long: 120.65, + lant: 28.01, + value: 0 + }, + %{ + city: "九江", + long: 115.97, + lant: 29.71, + value: 0 + }, + %{ + city: "邯郸", + long: 114.47, + lant: 36.6, + value: 0 + }, + %{ + city: "临安", + long: 119.72, + lant: 30.23, + value: 0 + }, + %{ + city: "兰州", + long: 103.73, + lant: 36.03, + value: 0 + }, + %{ + city: "沧州", + long: 116.83, + lant: 38.33, + value: 0 + }, + %{ + city: "临沂", + long: 118.35, + lant: 35.05, + value: 0 + }, + %{ + city: "南充", + long: 106.110698, + lant: 30.837793, + value: 0 + }, + %{ + city: "天津", + long: 117.2, + lant: 39.13, + value: 0 + }, + %{ + city: "富阳", + long: 119.95, + lant: 30.07, + value: 0 + }, + %{ + city: "泰安", + long: 117.13, + lant: 36.18, + value: 0 + }, + %{ + city: "诸暨", + long: 120.23, + lant: 29.71, + value: 0 + }, + %{ + city: "郑州", + long: 113.65, + lant: 34.76, + value: 0 + }, + %{ + city: "哈尔滨", + long: 126.63, + lant: 45.75, + value: 0 + }, + %{ + city: "聊城", + long: 115.97, + lant: 36.45, + value: 0 + }, + %{ + city: "芜湖", + long: 118.38, + lant: 31.33, + value: 0 + }, + %{ + city: "唐山", + long: 118.02, + lant: 39.63, + value: 0 + }, + %{ + city: "平顶山", + long: 113.29, + lant: 33.75, + value: 0 + }, + %{ + city: "邢台", + long: 114.48, + lant: 37.05, + value: 0 + }, + %{ + city: "德州", + long: 116.29, + lant: 37.45, + value: 0 + }, + %{ + city: "济宁", + long: 116.59, + lant: 35.38, + value: 0 + }, + %{ + city: "荆州", + long: 112.239741, + lant: 30.335165, + value: 0 + }, + %{ + city: "宜昌", + long: 111.3, + lant: 30.7, + value: 0 + }, + %{ + city: "义乌", + long: 120.06, + lant: 29.32, + value: 0 + }, + %{ + city: "丽水", + long: 119.92, + lant: 28.45, + value: 0 + }, + %{ + city: "洛阳", + long: 112.44, + lant: 34.7, + value: 0 + }, + %{ + city: "秦皇岛", + long: 119.57, + lant: 39.95, + value: 0 + }, + %{ + city: "株洲", + long: 113.16, + lant: 27.83, + value: 0 + }, + %{ + city: "石家庄", + long: 114.48, + lant: 38.03, + value: 0 + }, + %{ + city: "莱芜", + long: 117.67, + lant: 36.19, + value: 0 + }, + %{ + city: "常德", + long: 111.69, + lant: 29.05, + value: 0 + }, + %{ + city: "保定", + long: 115.48, + lant: 38.85, + value: 0 + }, + %{ + city: "湘潭", + long: 112.91, + lant: 27.87, + value: 0 + }, + %{ + city: "金华", + long: 119.64, + lant: 29.12, + value: 0 + }, + %{ + city: "岳阳", + long: 113.09, + lant: 29.37, + value: 0 + }, + %{ + city: "长沙", + long: 113, + lant: 28.21, + value: 0 + }, + %{ + city: "衢州", + long: 118.88, + lant: 28.97, + value: 0 + }, + %{ + city: "廊坊", + long: 116.7, + lant: 39.53, + value: 0 + }, + %{ + city: "菏泽", + long: 115.480656, + lant: 35.23375, + value: 0 + }, + %{ + city: "合肥", + long: 117.27, + lant: 31.86, + value: 0 + }, + %{ + city: "武汉", + long: 114.31, + lant: 30.52, + value: 0 + }, + %{ + city: "大庆", + long: 125.03, + lant: 46.58, + value: 0 + } + ] + end +end diff --git a/test/support/test_tools.ex b/test/support/test_tools.ex index cc640ffb1..ef77bc2b7 100644 --- a/test/support/test_tools.ex +++ b/test/support/test_tools.ex @@ -8,7 +8,7 @@ defmodule MastaniServer.TestTools do quote do use MastaniServerWeb.ConnCase, async: true - import MastaniServer.Factory + import MastaniServer.Support.Factory import MastaniServer.Test.ConnSimulator import MastaniServer.Test.AssertHelper import Ecto.Query, warn: false From 584fcab3b66c952a193e11f820b94cfddbe99a35 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 26 Sep 2018 23:26:20 +0800 Subject: [PATCH 017/129] chore(wip clean up): clean up wip --- test/mastani_server/accounts/accounts_test.exs | 2 -- test/mastani_server/statistics/geo_test.exs | 3 --- test/mastani_server_web/mutation/cms/cms_test.exs | 3 --- test/mastani_server_web/query/accounts/account_test.exs | 1 - test/mastani_server_web/query/cms/cms_geo_test.exs | 2 -- test/mastani_server_web/query/statistics/statistics_test.exs | 1 - 6 files changed, 12 deletions(-) diff --git a/test/mastani_server/accounts/accounts_test.exs b/test/mastani_server/accounts/accounts_test.exs index 24b027397..d379e3a9e 100644 --- a/test/mastani_server/accounts/accounts_test.exs +++ b/test/mastani_server/accounts/accounts_test.exs @@ -128,7 +128,6 @@ defmodule MastaniServer.Test.Accounts do assert ORM.count(GithubUser) == 1 end - @tag :wip test "github signin user should be locate geo city info" do {:ok, guser} = Accounts.github_signin(@valid_github_profile) {:ok, user} = ORM.find(User, guser.user.id) @@ -136,7 +135,6 @@ defmodule MastaniServer.Test.Accounts do assert user.geo_city !== nil end - @tag :wip test "github signin user from invalid ip locate geo city fails" do {:ok, guser} = Accounts.github_signin(@valid_github_profile, :fake_ip) {:ok, user} = ORM.find(User, guser.user.id) diff --git a/test/mastani_server/statistics/geo_test.exs b/test/mastani_server/statistics/geo_test.exs index 827e16cf2..36db28524 100644 --- a/test/mastani_server/statistics/geo_test.exs +++ b/test/mastani_server/statistics/geo_test.exs @@ -11,7 +11,6 @@ defmodule MastaniServer.Test.Statistics.Geo do end describe "[statistics geo inc] " do - @tag :wip test "geo data can be inc by city" do {:ok, _} = Statistics.UserGeoInfo |> ORM.find_by(%{city: "成都"}) @@ -25,14 +24,12 @@ defmodule MastaniServer.Test.Statistics.Geo do assert updated.value == 2 end - @tag :wip test "inc with invalid city fails" do assert {:error, _} = Statistics.inc_count("not_exsit") end end describe "[statistics geo get] " do - @tag :wip test "can get geo citis info" do {:ok, infos} = Statistics.list_cities_info() assert infos.total_count == 0 diff --git a/test/mastani_server_web/mutation/cms/cms_test.exs b/test/mastani_server_web/mutation/cms/cms_test.exs index cc5b2c62a..f675656cb 100644 --- a/test/mastani_server_web/mutation/cms/cms_test.exs +++ b/test/mastani_server_web/mutation/cms/cms_test.exs @@ -636,7 +636,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do } } """ - @tag :wip test "login user can subscribe community", ~m(user community)a do login_conn = simu_conn(:user, user) @@ -660,7 +659,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do assert guest_conn |> mutation_get_error?(@subscribe_query, variables, ecode(:account_login)) end - @tag :wip test "subscribed community should inc it's own geo info", ~m(user community)a do login_conn = simu_conn(:user, user) @@ -724,7 +722,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do |> mutation_get_error?(@unsubscribe_query, variables, ecode(:account_login)) end - @tag :wip test "unsubscribed community should dec it's own geo info", ~m(user community)a do login_conn = simu_conn(:user, user) diff --git a/test/mastani_server_web/query/accounts/account_test.exs b/test/mastani_server_web/query/accounts/account_test.exs index d8ea1194b..c01dd5f19 100644 --- a/test/mastani_server_web/query/accounts/account_test.exs +++ b/test/mastani_server_web/query/accounts/account_test.exs @@ -24,7 +24,6 @@ defmodule MastaniServer.Test.Query.Account.Basic do } } """ - @tag :wip test "guest user should get false sessionState", ~m(guest_conn)a do results = guest_conn |> query_result(@query, %{}, "sessionState") assert results["isValid"] == false diff --git a/test/mastani_server_web/query/cms/cms_geo_test.exs b/test/mastani_server_web/query/cms/cms_geo_test.exs index 2d7cdec16..8ea37a57b 100644 --- a/test/mastani_server_web/query/cms/cms_geo_test.exs +++ b/test/mastani_server_web/query/cms/cms_geo_test.exs @@ -21,7 +21,6 @@ defmodule MastaniServer.Test.Query.CMS.GEO do } } """ - @tag :wip test "empty community should get empty geo info", ~m(guest_conn community)a do variables = %{id: community.id} results = guest_conn |> query_result(@query, variables, "communityGeoInfo") @@ -29,7 +28,6 @@ defmodule MastaniServer.Test.Query.CMS.GEO do assert results == [] end - @tag :wip test "community should get geo info after subscribe", ~m(guest_conn community user)a do {:ok, _record} = CMS.subscribe_community(community, user) diff --git a/test/mastani_server_web/query/statistics/statistics_test.exs b/test/mastani_server_web/query/statistics/statistics_test.exs index a1c2141f6..3d6e685ab 100644 --- a/test/mastani_server_web/query/statistics/statistics_test.exs +++ b/test/mastani_server_web/query/statistics/statistics_test.exs @@ -47,7 +47,6 @@ defmodule MastaniServer.Test.Query.Statistics do } """ describe "[statistics geo info]" do - @tag :wip test "should get cities geo infos", ~m(guest_conn)a do result = guest_conn |> query_result(@query, %{}, "citiesGeoInfo") assert result["entries"] == [] From 5b635cdd6f5a8d4505b8a177d8c69b9828c10baa Mon Sep 17 00:00:00 2001 From: xieyiming Date: Fri, 28 Sep 2018 13:43:23 +0800 Subject: [PATCH 018/129] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c295a1c37..225d7e6d8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -![CPS Brand](https://github.com/mydearxym/mastani_server/blob/dev/docs/snapshots/cps_logo_md.png) +![CPS Brand](https://github.com/mydearxym/mastani_server/blob/dev/docs/snapshots/cps_logo_md.png?raw=true) ========= [![Build Status](https://travis-ci.org/coderplanets/coderplanets_server.svg?branch=dev)](https://travis-ci.org/coderplanets/coderplanets_server) [![codecov](https://codecov.io/gh/coderplanets/coderplanets_server/branch/dev/graph/badge.svg)](https://codecov.io/gh/coderplanets/coderplanets_server) From b5d07cfb3b4426917359793f1a2fe8f4e118a2ba Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 28 Sep 2018 18:33:36 +0800 Subject: [PATCH 019/129] fix: add missing cms_thread --- lib/mastani_server_web/schema/cms/cms_misc.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index bd486888a..c163bf360 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -37,6 +37,7 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do enum :cms_thread do value(:post) value(:job) + value(:user) value(:video) value(:repo) value(:wiki) From a32ffdcb5da54cf297aa2fdc432e550f2d3cce96 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 28 Sep 2018 23:12:22 +0800 Subject: [PATCH 020/129] refactor(cms.repo): use new repo fields --- lib/mastani_server/cms/repo.ex | 34 ++++++++++-------- lib/mastani_server/cms/repo_builder.ex | 26 -------------- lib/mastani_server/cms/repo_contributor.ex | 23 ++++++++++++ .../schema/cms/cms_types.ex | 25 +++++++------ .../migrations/20180928135647_alter_repos.exs | 36 +++++++++++++++++++ .../20180928141144_rename_repos_fiels.exs | 12 +++++++ test/mastani_server/cms/repo_test.exs | 2 +- .../query/cms/repo_test.exs | 4 +-- test/support/factory.ex | 20 ++++++----- 9 files changed, 120 insertions(+), 62 deletions(-) delete mode 100644 lib/mastani_server/cms/repo_builder.ex create mode 100644 lib/mastani_server/cms/repo_contributor.ex create mode 100644 priv/repo/migrations/20180928135647_alter_repos.exs create mode 100644 priv/repo/migrations/20180928141144_rename_repos_fiels.exs diff --git a/lib/mastani_server/cms/repo.ex b/lib/mastani_server/cms/repo.ex index cc7df3010..1a6331161 100644 --- a/lib/mastani_server/cms/repo.ex +++ b/lib/mastani_server/cms/repo.ex @@ -4,29 +4,35 @@ defmodule MastaniServer.CMS.Repo do use Ecto.Schema import Ecto.Changeset - alias MastaniServer.CMS.{Author, Community, RepoBuilder, RepoCommunityFlag, Tag} + alias MastaniServer.CMS.{Author, Community, RepoContributor, RepoCommunityFlag, Tag} - @required_fields ~w(repo_name desc readme language producer producer_link repo_link repo_star_count repo_fork_count repo_watch_count)a - @optional_fields ~w(views last_fetch_time) + @required_fields ~w(title owner_name owner_url repo_url desc homepage_url readme issues_count prs_count fork_count watch_count primary_language license release_tag)a + @optional_fields ~w(last_fetch_time) @type t :: %Repo{} schema "cms_repos" do - field(:repo_name, :string) + field(:title, :string) + field(:owner_name, :string) + field(:owner_url, :string) + field(:repo_url, :string) + field(:desc, :string) + field(:homepage_url, :string) field(:readme, :string) - field(:language, :string) - belongs_to(:author, Author) - field(:repo_link, :string) - field(:producer, :string) - field(:producer_link, :string) + field(:issues_count, :integer) + field(:prs_count, :integer) + field(:fork_count, :integer) + field(:watch_count, :integer) - field(:repo_star_count, :integer) - field(:repo_fork_count, :integer) - field(:repo_watch_count, :integer) + field(:primary_language, :string) + field(:license, :string) + field(:release_tag, :string) - field(:views, :integer, default: 0) + embeds_many(:contributors, RepoContributor) + field(:views, :integer, default: 0) + belongs_to(:author, Author) has_many(:community_flags, {"repos_communities_flags", RepoCommunityFlag}) # NOTE: this one is tricky, pin is dynamic changed when return by func: add_pin_contents_ifneed @@ -34,8 +40,6 @@ defmodule MastaniServer.CMS.Repo do field(:trash, :boolean, default_value: false) field(:last_fetch_time, :utc_datetime) - # TODO: replace RepoBuilder with paged user map - has_many(:builders, {"repos_builders", RepoBuilder}) many_to_many( :tags, diff --git a/lib/mastani_server/cms/repo_builder.ex b/lib/mastani_server/cms/repo_builder.ex deleted file mode 100644 index dcc3fb4a7..000000000 --- a/lib/mastani_server/cms/repo_builder.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule MastaniServer.CMS.RepoBuilder do - @moduledoc false - alias __MODULE__ - - use Ecto.Schema - import Ecto.Changeset - - @required_fields ~w(nickname avatar link)a - @optional_fields ~w(bio) - - @type t :: %RepoBuilder{} - schema "cms_repo_users" do - field(:nickname, :string) - field(:avatar, :string) - field(:link, :string) - - timestamps(type: :utc_datetime) - end - - @doc false - def changeset(%RepoBuilder{} = repo_builder, attrs) do - repo_builder - |> cast(attrs, @optional_fields ++ @required_fields) - |> validate_required(@required_fields) - end -end diff --git a/lib/mastani_server/cms/repo_contributor.ex b/lib/mastani_server/cms/repo_contributor.ex new file mode 100644 index 000000000..ffac880c8 --- /dev/null +++ b/lib/mastani_server/cms/repo_contributor.ex @@ -0,0 +1,23 @@ +defmodule MastaniServer.CMS.RepoContributor do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + + @required_fields ~w(avatar nickname html_url)a + + @type t :: %RepoContributor{} + embedded_schema do + field(:avatar, :string) + field(:nickname, :string) + field(:html_url, :string) + end + + @doc false + def changeset(%RepoContributor{} = repo_contributor, attrs) do + repo_contributor + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index a2dfe328e..92eb1c8a7 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -253,21 +253,26 @@ defmodule MastaniServerWeb.Schema.CMS.Types do object :repo do # interface(:article) field(:id, :id) - field(:repo_name, :string) + field(:title, :string) + field(:owner_name, :string) + field(:owner_url, :string) + field(:repo_url, :string) + field(:desc, :string) + field(:homepage_url, :string) field(:readme, :string) - field(:language, :string) - field(:author, :user, resolve: dataloader(CMS, :author)) - field(:repo_link, :string) - field(:producer, :string) - field(:producer_link, :integer) + field(:issues_count, :integer) + field(:prs_count, :integer) + field(:fork_count, :integer) + field(:watch_count, :integer) - field(:repo_star_count, :integer) - field(:repo_fork_count, :integer) - field(:repo_watch_count, :integer) - field(:views, :integer) + field(:primary_language, :string) + field(:license, :string) + field(:release_tag, :string) + field(:author, :user, resolve: dataloader(CMS, :author)) + field(:views, :integer) field(:pin, :boolean) field(:trash, :boolean) # TODO: remove diff --git a/priv/repo/migrations/20180928135647_alter_repos.exs b/priv/repo/migrations/20180928135647_alter_repos.exs new file mode 100644 index 000000000..64dacac76 --- /dev/null +++ b/priv/repo/migrations/20180928135647_alter_repos.exs @@ -0,0 +1,36 @@ +defmodule MastaniServer.Repo.Migrations.AlterRepos do + use Ecto.Migration + + def change do + alter table(:cms_repos) do + add(:title, :string) + add(:owner_name, :string) + add(:owner_url, :string) + add(:repo_url, :string) + + add(:homepage_url, :string) + + add(:issuesCount, :integer) + add(:prsCount, :integer) + add(:forkCount, :integer) + add(:watchCount, :integer) + + add(:primary_language, :string) + add(:license, :string) + add(:releaseTag, :string) + + add(:contributors, :map) + + remove(:repo_name) + remove(:repo_link) + + remove(:language) + remove(:producer) + remove(:producer_link) + + remove(:repo_star_count) + remove(:repo_fork_count) + remove(:repo_watch_count) + end + end +end diff --git a/priv/repo/migrations/20180928141144_rename_repos_fiels.exs b/priv/repo/migrations/20180928141144_rename_repos_fiels.exs new file mode 100644 index 000000000..2b3a2a854 --- /dev/null +++ b/priv/repo/migrations/20180928141144_rename_repos_fiels.exs @@ -0,0 +1,12 @@ +defmodule MastaniServer.Repo.Migrations.RenameReposFiels do + use Ecto.Migration + + def change do + rename(table(:cms_repos), :issuesCount, to: :issues_count) + rename(table(:cms_repos), :prsCount, to: :prs_count) + rename(table(:cms_repos), :forkCount, to: :fork_count) + rename(table(:cms_repos), :watchCount, to: :watch_count) + + rename(table(:cms_repos), :releaseTag, to: :release_tag) + end +end diff --git a/test/mastani_server/cms/repo_test.exs b/test/mastani_server/cms/repo_test.exs index c42e03a8a..0ff261cb2 100644 --- a/test/mastani_server/cms/repo_test.exs +++ b/test/mastani_server/cms/repo_test.exs @@ -21,7 +21,7 @@ defmodule MastaniServer.Test.Repo do assert {:error, _} = ORM.find_by(Author, user_id: user.id) {:ok, repo} = CMS.create_content(community, :repo, repo_attrs, user) - assert repo.repo_name == repo_attrs.repo_name + assert repo.title == repo_attrs.title end test "add user to cms authors, if the user is not exsit in cms authors", diff --git a/test/mastani_server_web/query/cms/repo_test.exs b/test/mastani_server_web/query/cms/repo_test.exs index 0de2ce92b..c2aaa3948 100644 --- a/test/mastani_server_web/query/cms/repo_test.exs +++ b/test/mastani_server_web/query/cms/repo_test.exs @@ -14,7 +14,7 @@ defmodule MastaniServer.Test.Query.Repo do query($id: ID!) { repo(id: $id) { id - repo_name + title } } """ @@ -23,7 +23,7 @@ defmodule MastaniServer.Test.Query.Repo do results = guest_conn |> query_result(@query, variables, "repo") assert results["id"] == to_string(repo.id) - assert is_valid_kv?(results, "repo_name", :string) + assert is_valid_kv?(results, "title", :string) assert length(Map.keys(results)) == 2 end diff --git a/test/support/factory.ex b/test/support/factory.ex index d20c5964d..437a6eb57 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -59,17 +59,21 @@ defmodule MastaniServer.Support.Factory do desc = Faker.Lorem.sentence(%Range{first: 15, last: 60}) %{ - repo_name: Faker.Lorem.Shakespeare.king_richard_iii(), + title: Faker.Lorem.Shakespeare.king_richard_iii(), + owner_name: "coderplanets", + owner_url: "http://www.github.com/coderplanets", + repo_url: "http://www.github.com/coderplanets//coderplanets_server", desc: desc, + homepage_url: "http://www.github.com/coderplanets", readme: desc, - language: "javascript", + issues_count: Enum.random(0..2000), + prs_count: Enum.random(0..2000), + fork_count: Enum.random(0..2000), + watch_count: Enum.random(0..2000), + primary_language: "javascript", + license: "MIT", + release_tag: "v22", author: mock(:author), - repo_link: "http://www.github.com/mydearxym", - producer: "mydearxym", - producer_link: "http://www.github.com/mydearxym", - repo_star_count: Enum.random(0..2000), - repo_fork_count: Enum.random(0..2000), - repo_watch_count: Enum.random(0..2000), views: Enum.random(0..2000), communities: [ mock(:community), From 76ead2f17d4e19d555675690ea4b8dda8d830b8f Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 29 Sep 2018 11:43:36 +0800 Subject: [PATCH 021/129] refactor(repo thread): change fields to match frontend --- .../cms/delegates/article_curd.ex | 19 ++++- lib/mastani_server/cms/repo.ex | 6 +- lib/mastani_server_web/schema/cms/cms_misc.ex | 9 +++ .../schema/cms/mutations/repo.ex | 30 +++++-- test/mastani_server/cms/repo_test.exs | 2 + .../mutation/cms/repo_test.exs | 79 +++++++++++++++++++ test/support/factory.ex | 28 ++++++- 7 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 test/mastani_server_web/mutation/cms/repo_test.exs diff --git a/lib/mastani_server/cms/delegates/article_curd.ex b/lib/mastani_server/cms/delegates/article_curd.ex index 08b917688..4cc713e2c 100644 --- a/lib/mastani_server/cms/delegates/article_curd.ex +++ b/lib/mastani_server/cms/delegates/article_curd.ex @@ -8,8 +8,10 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do import Helper.ErrorCode import ShortMaps + alias MastaniServer.Repo + alias MastaniServer.Accounts.User - alias MastaniServer.{CMS, Repo, Statistics} + alias MastaniServer.{CMS, Statistics} alias CMS.Delegate.ArticleOperation alias Helper.{ORM, QueryBuilder} @@ -144,7 +146,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do defp create_content_result({:ok, %{add_content_author: result}}), do: {:ok, result} defp create_content_result({:error, :add_content_author, _result, _steps}) do - {:error, [message: "assign author", code: ecode(:create_fails)]} + {:error, [message: "create cms content author", code: ecode(:create_fails)]} end defp create_content_result({:error, :set_community, _result, _steps}) do @@ -178,6 +180,19 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do end end + @doc """ + update CMS repo(github) + """ + alias MastaniServer.CMS + + defp update_repo(%CMS.Repo{} = repo, attrs \\ %{}) do + repo + |> Ecto.Changeset.change(attrs) + |> Ecto.Changeset.put_embed(:contributors, attrs.contributors) + |> MastaniServer.CMS.Repo.update_changeset(attrs) + |> Repo.update() + end + @doc """ get CMS contents post's favorites/stars/comments ... diff --git a/lib/mastani_server/cms/repo.ex b/lib/mastani_server/cms/repo.ex index 1a6331161..30bca18db 100644 --- a/lib/mastani_server/cms/repo.ex +++ b/lib/mastani_server/cms/repo.ex @@ -8,6 +8,7 @@ defmodule MastaniServer.CMS.Repo do @required_fields ~w(title owner_name owner_url repo_url desc homepage_url readme issues_count prs_count fork_count watch_count primary_language license release_tag)a @optional_fields ~w(last_fetch_time) + @contributors_field ~w(contributors) @type t :: %Repo{} schema "cms_repos" do @@ -29,7 +30,7 @@ defmodule MastaniServer.CMS.Repo do field(:license, :string) field(:release_tag, :string) - embeds_many(:contributors, RepoContributor) + embeds_many(:contributors, RepoContributor, on_replace: :delete) field(:views, :integer, default: 0) belongs_to(:author, Author) @@ -64,8 +65,11 @@ defmodule MastaniServer.CMS.Repo do def changeset(%Repo{} = repo, attrs) do repo |> cast(attrs, @optional_fields ++ @required_fields) + |> cast_embed(:contributors, with: &RepoContributor.changeset/2) |> validate_required(@required_fields) + # |> cast_embed(:contributors, with: &RepoContributor.changeset/2) + # |> foreign_key_constraint(:posts_tags, name: :posts_tags_tag_id_fkey) # |> foreign_key_constraint(name: :posts_tags_tag_id_fkey) end diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index c163bf360..fc3342c03 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -155,6 +155,15 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do # field(:tag, :string, default_value: :all) end + @doc """ + cms github repo contribotor + """ + input_object :repo_contributor_input do + field(:avatar, :string) + field(:html_url, :string) + field(:nickname, :string) + end + @doc """ only used for reaction result, like: favorite/star/watch ... """ diff --git a/lib/mastani_server_web/schema/cms/mutations/repo.ex b/lib/mastani_server_web/schema/cms/mutations/repo.ex index 7285ba85e..5c83edac7 100644 --- a/lib/mastani_server_web/schema/cms/mutations/repo.ex +++ b/lib/mastani_server_web/schema/cms/mutations/repo.ex @@ -7,19 +7,33 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Repo do object :cms_repo_mutations do @desc "create a user" field :create_repo, :repo do - arg(:repo_name, non_null(:string)) + arg(:title, non_null(:string)) + arg(:owner_name, non_null(:string)) + arg(:owner_url, non_null(:string)) + arg(:repo_url, non_null(:string)) + arg(:desc, non_null(:string)) + arg(:homepage_url, non_null(:string)) arg(:readme, non_null(:string)) - arg(:language, non_null(:string)) - arg(:repo_link, non_null(:string)) - arg(:producer, non_null(:string)) - arg(:producer_link, non_null(:string)) - arg(:repo_star_count, non_null(:integer)) - arg(:repo_fork_count, non_null(:integer)) - arg(:repo_watch_count, non_null(:integer)) + arg(:issues_count, non_null(:integer)) + arg(:prs_count, non_null(:integer)) + arg(:fork_count, non_null(:integer)) + arg(:watch_count, non_null(:integer)) + + arg(:primary_language, non_null(:string)) + arg(:license, non_null(:string)) + arg(:release_tag, non_null(:string)) + + arg(:contributors, list_of(:repo_contributor_input)) + + arg(:community_id, non_null(:id)) + arg(:thread, :cms_thread, default_value: :repo) + arg(:tags, list_of(:ids)) middleware(M.Authorize, :login) + middleware(M.PublishThrottle) + resolve(&R.CMS.create_content/3) end diff --git a/test/mastani_server/cms/repo_test.exs b/test/mastani_server/cms/repo_test.exs index 0ff261cb2..fb568cc70 100644 --- a/test/mastani_server/cms/repo_test.exs +++ b/test/mastani_server/cms/repo_test.exs @@ -21,7 +21,9 @@ defmodule MastaniServer.Test.Repo do assert {:error, _} = ORM.find_by(Author, user_id: user.id) {:ok, repo} = CMS.create_content(community, :repo, repo_attrs, user) + assert repo.title == repo_attrs.title + assert repo.contributors |> length !== 0 end test "add user to cms authors, if the user is not exsit in cms authors", diff --git a/test/mastani_server_web/mutation/cms/repo_test.exs b/test/mastani_server_web/mutation/cms/repo_test.exs new file mode 100644 index 000000000..c7b177122 --- /dev/null +++ b/test/mastani_server_web/mutation/cms/repo_test.exs @@ -0,0 +1,79 @@ +defmodule MastaniServer.Test.Mutation.Repo do + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.CMS + + setup do + {:ok, repo} = db_insert(:repo) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user) + owner_conn = simu_conn(:owner, repo) + + {:ok, ~m(user_conn guest_conn owner_conn repo)a} + end + + describe "[mutation repo curd]" do + @create_repo_query """ + mutation( + $title: String!, + $ownerName: String!, + $ownerUrl: String!, + $repoUrl: String!, + $desc: String!, + $homepageUrl: Int!, + $readme: String!, + $issuesCount: Int!, + $prsCount: Int!, + $forkCount: Int!, + $watchCount: Int!, + $primaryLanguage: String!, + $license: String!, + $releaseTag: String!, + $contributors: [RepoContributorInput], + $communityId: ID!, + $tags: [Ids] + ) { + createRepo( + title: $title, + ownerName: $ownerName, + ownerUrl: $ownerUrl, + repoUrl: $repoUrl, + desc: $desc, + homepageUrl: $homepageUrl, + readme: $readme, + issuesCount: $issuesCount, + prsCount: $prsCount, + forkCount: $forkCount, + watchCount: $watchCount, + primaryLanguage: $primaryLanguage, + license: $license, + releaseTag: $releaseTag, + contributors: $contributors, + communityId: $communityId, + tags: $tags + ) { + id + title + desc + } + } + """ + test "create repo with valid attrs and make sure author exsit" do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + {:ok, community} = db_insert(:community) + repo_attr = mock_attrs(:repo) + + variables = repo_attr |> Map.merge(%{communityId: community.id}) + created = user_conn |> mutation_result(@create_repo_query, variables, "createRepo") + {:ok, repo} = ORM.find(CMS.Repo, created["id"]) + # IO.inspect repo, label: "hello" + + assert created["id"] == to_string(repo.id) + assert {:ok, _} = ORM.find_by(CMS.Author, user_id: user.id) + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index 437a6eb57..62a57ae27 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -40,8 +40,8 @@ defmodule MastaniServer.Support.Factory do durationSec: Enum.random(300..12_000), source: "youtube", link: "http://www.youtube.com/video/1", - original_author: "simon", - originalAuthor: "simon", + original_author: "mydearxym", + originalAuthor: "mydearxym", original_author_link: "http://www.youtube.com/user/1", originalAuthorLink: "http://www.youtube.com/user/1", author: mock(:author), @@ -61,18 +61,42 @@ defmodule MastaniServer.Support.Factory do %{ title: Faker.Lorem.Shakespeare.king_richard_iii(), owner_name: "coderplanets", + ownerName: "coderplanets", owner_url: "http://www.github.com/coderplanets", + ownerUrl: "http://www.github.com/coderplanets", repo_url: "http://www.github.com/coderplanets//coderplanets_server", + repoUrl: "http://www.github.com/coderplanets//coderplanets_server", desc: desc, homepage_url: "http://www.github.com/coderplanets", + homepageUrl: "http://www.github.com/coderplanets", readme: desc, issues_count: Enum.random(0..2000), + issuesCount: Enum.random(0..2000), prs_count: Enum.random(0..2000), + prsCount: Enum.random(0..2000), fork_count: Enum.random(0..2000), + forkCount: Enum.random(0..2000), watch_count: Enum.random(0..2000), + watchCount: Enum.random(0..2000), primary_language: "javascript", + primaryLanguage: "javascript", license: "MIT", release_tag: "v22", + releaseTag: "v22", + contributors: [ + %{ + avatar: Faker.Avatar.image_url(), + html_url: Faker.Avatar.image_url(), + htmlUrl: Faker.Avatar.image_url(), + nickname: "mydearxym" + }, + %{ + avatar: Faker.Avatar.image_url(), + html_url: Faker.Avatar.image_url(), + htmlUrl: Faker.Avatar.image_url(), + nickname: "mydearxym2" + } + ], author: mock(:author), views: Enum.random(0..2000), communities: [ From c932c2676768ed1f30e610db31fbf1b5e70f6e42 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 29 Sep 2018 19:58:24 +0800 Subject: [PATCH 022/129] feat(repo thread): debug with frontend, feature is done --- .../cms/delegates/article_curd.ex | 14 +----------- lib/mastani_server/cms/repo.ex | 16 +++++--------- lib/mastani_server/cms/repo_lang.ex | 22 +++++++++++++++++++ lib/mastani_server_web/schema/cms/cms_misc.ex | 8 +++++++ .../schema/cms/cms_types.ex | 16 +++++++++++++- .../schema/cms/mutations/repo.ex | 9 ++++---- priv/mock/cms_repo_seeds.exs | 2 +- ...044158_add_star_language_info_to_repos.exs | 11 ++++++++++ .../mutation/cms/repo_test.exs | 10 +++++---- test/support/factory.ex | 10 +++++++++ 10 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 lib/mastani_server/cms/repo_lang.ex create mode 100644 priv/repo/migrations/20180929044158_add_star_language_info_to_repos.exs diff --git a/lib/mastani_server/cms/delegates/article_curd.ex b/lib/mastani_server/cms/delegates/article_curd.ex index 4cc713e2c..22e093c2a 100644 --- a/lib/mastani_server/cms/delegates/article_curd.ex +++ b/lib/mastani_server/cms/delegates/article_curd.ex @@ -145,6 +145,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do defp create_content_result({:ok, %{add_content_author: result}}), do: {:ok, result} + # TODO: need more spec error handle defp create_content_result({:error, :add_content_author, _result, _steps}) do {:error, [message: "create cms content author", code: ecode(:create_fails)]} end @@ -180,19 +181,6 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do end end - @doc """ - update CMS repo(github) - """ - alias MastaniServer.CMS - - defp update_repo(%CMS.Repo{} = repo, attrs \\ %{}) do - repo - |> Ecto.Changeset.change(attrs) - |> Ecto.Changeset.put_embed(:contributors, attrs.contributors) - |> MastaniServer.CMS.Repo.update_changeset(attrs) - |> Repo.update() - end - @doc """ get CMS contents post's favorites/stars/comments ... diff --git a/lib/mastani_server/cms/repo.ex b/lib/mastani_server/cms/repo.ex index 30bca18db..d09f85226 100644 --- a/lib/mastani_server/cms/repo.ex +++ b/lib/mastani_server/cms/repo.ex @@ -4,11 +4,10 @@ defmodule MastaniServer.CMS.Repo do use Ecto.Schema import Ecto.Changeset - alias MastaniServer.CMS.{Author, Community, RepoContributor, RepoCommunityFlag, Tag} + alias MastaniServer.CMS.{Author, Community, RepoContributor, RepoLang, RepoCommunityFlag, Tag} - @required_fields ~w(title owner_name owner_url repo_url desc homepage_url readme issues_count prs_count fork_count watch_count primary_language license release_tag)a - @optional_fields ~w(last_fetch_time) - @contributors_field ~w(contributors) + @required_fields ~w(title owner_name owner_url repo_url desc readme star_count issues_count prs_count fork_count watch_count)a + @optional_fields ~w(last_fetch_time homepage_url release_tag license) @type t :: %Repo{} schema "cms_repos" do @@ -21,14 +20,15 @@ defmodule MastaniServer.CMS.Repo do field(:homepage_url, :string) field(:readme, :string) + field(:star_count, :integer) field(:issues_count, :integer) field(:prs_count, :integer) field(:fork_count, :integer) field(:watch_count, :integer) - field(:primary_language, :string) field(:license, :string) field(:release_tag, :string) + embeds_one(:primary_language, RepoLang, on_replace: :delete) embeds_many(:contributors, RepoContributor, on_replace: :delete) @@ -66,11 +66,7 @@ defmodule MastaniServer.CMS.Repo do repo |> cast(attrs, @optional_fields ++ @required_fields) |> cast_embed(:contributors, with: &RepoContributor.changeset/2) + |> cast_embed(:primary_language, with: &RepoLang.changeset/2) |> validate_required(@required_fields) - - # |> cast_embed(:contributors, with: &RepoContributor.changeset/2) - - # |> foreign_key_constraint(:posts_tags, name: :posts_tags_tag_id_fkey) - # |> foreign_key_constraint(name: :posts_tags_tag_id_fkey) end end diff --git a/lib/mastani_server/cms/repo_lang.ex b/lib/mastani_server/cms/repo_lang.ex new file mode 100644 index 000000000..4df263f73 --- /dev/null +++ b/lib/mastani_server/cms/repo_lang.ex @@ -0,0 +1,22 @@ +defmodule MastaniServer.CMS.RepoLang do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + + @required_fields ~w(name color)a + + @type t :: %RepoLang{} + embedded_schema do + field(:name, :string) + field(:color, :string) + end + + @doc false + def changeset(%RepoLang{} = repo_lang, attrs) do + repo_lang + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index fc3342c03..940214a23 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -164,6 +164,14 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do field(:nickname, :string) end + @doc """ + cms github repo lang + """ + input_object :repo_lang_input do + field(:name, :string) + field(:color, :string) + end + @doc """ only used for reaction result, like: favorite/star/watch ... """ diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 92eb1c8a7..f85b07f44 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -250,6 +250,17 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:updated_at, :datetime) end + object :repo_contributor do + field(:avatar, :string) + field(:html_url, :string) + field(:nickname, :string) + end + + object :repo_lang do + field(:name, :string) + field(:color, :string) + end + object :repo do # interface(:article) field(:id, :id) @@ -262,15 +273,18 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:homepage_url, :string) field(:readme, :string) + field(:star_count, :integer) field(:issues_count, :integer) field(:prs_count, :integer) field(:fork_count, :integer) field(:watch_count, :integer) - field(:primary_language, :string) + field(:primary_language, :repo_lang) field(:license, :string) field(:release_tag, :string) + field(:contributors, list_of(:repo_contributor)) + field(:author, :user, resolve: dataloader(CMS, :author)) field(:views, :integer) field(:pin, :boolean) diff --git a/lib/mastani_server_web/schema/cms/mutations/repo.ex b/lib/mastani_server_web/schema/cms/mutations/repo.ex index 5c83edac7..a8a4d8307 100644 --- a/lib/mastani_server_web/schema/cms/mutations/repo.ex +++ b/lib/mastani_server_web/schema/cms/mutations/repo.ex @@ -13,19 +13,20 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Repo do arg(:repo_url, non_null(:string)) arg(:desc, non_null(:string)) - arg(:homepage_url, non_null(:string)) + arg(:homepage_url, :string) arg(:readme, non_null(:string)) + arg(:star_count, non_null(:integer)) arg(:issues_count, non_null(:integer)) arg(:prs_count, non_null(:integer)) arg(:fork_count, non_null(:integer)) arg(:watch_count, non_null(:integer)) - arg(:primary_language, non_null(:string)) - arg(:license, non_null(:string)) - arg(:release_tag, non_null(:string)) + arg(:license, :string) + arg(:release_tag, :string) arg(:contributors, list_of(:repo_contributor_input)) + arg(:primary_language, non_null(:repo_lang_input)) arg(:community_id, non_null(:id)) arg(:thread, :cms_thread, default_value: :repo) diff --git a/priv/mock/cms_repo_seeds.exs b/priv/mock/cms_repo_seeds.exs index 1f84de77e..3c1c00721 100644 --- a/priv/mock/cms_repo_seeds.exs +++ b/priv/mock/cms_repo_seeds.exs @@ -1,3 +1,3 @@ import MastaniServer.Support.Factory -db_insert_multi(:repo, 5) +db_insert_multi(:repo, 1) diff --git a/priv/repo/migrations/20180929044158_add_star_language_info_to_repos.exs b/priv/repo/migrations/20180929044158_add_star_language_info_to_repos.exs new file mode 100644 index 000000000..2485df994 --- /dev/null +++ b/priv/repo/migrations/20180929044158_add_star_language_info_to_repos.exs @@ -0,0 +1,11 @@ +defmodule MastaniServer.Repo.Migrations.AddStarLanguageInfoToRepos do + use Ecto.Migration + + def change do + alter table(:cms_repos) do + add(:star_count, :integer) + remove(:primary_language) + add(:primary_language, :map) + end + end +end diff --git a/test/mastani_server_web/mutation/cms/repo_test.exs b/test/mastani_server_web/mutation/cms/repo_test.exs index c7b177122..721375b6c 100644 --- a/test/mastani_server_web/mutation/cms/repo_test.exs +++ b/test/mastani_server_web/mutation/cms/repo_test.exs @@ -22,15 +22,16 @@ defmodule MastaniServer.Test.Mutation.Repo do $ownerUrl: String!, $repoUrl: String!, $desc: String!, - $homepageUrl: Int!, + $homepageUrl: String, $readme: String!, + $starCount: Int!, $issuesCount: Int!, $prsCount: Int!, $forkCount: Int!, $watchCount: Int!, - $primaryLanguage: String!, - $license: String!, - $releaseTag: String!, + $license: String, + $releaseTag: String, + $primaryLanguage: RepoLangInput, $contributors: [RepoContributorInput], $communityId: ID!, $tags: [Ids] @@ -43,6 +44,7 @@ defmodule MastaniServer.Test.Mutation.Repo do desc: $desc, homepageUrl: $homepageUrl, readme: $readme, + starCount: $starCount, issuesCount: $issuesCount, prsCount: $prsCount, forkCount: $forkCount, diff --git a/test/support/factory.ex b/test/support/factory.ex index 62a57ae27..1924fa8fe 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -76,6 +76,8 @@ defmodule MastaniServer.Support.Factory do prsCount: Enum.random(0..2000), fork_count: Enum.random(0..2000), forkCount: Enum.random(0..2000), + star_count: Enum.random(0..2000), + starCount: Enum.random(0..2000), watch_count: Enum.random(0..2000), watchCount: Enum.random(0..2000), primary_language: "javascript", @@ -83,6 +85,14 @@ defmodule MastaniServer.Support.Factory do license: "MIT", release_tag: "v22", releaseTag: "v22", + primary_language: %{ + name: "javascript", + color: "tomato" + }, + primaryLanguage: %{ + name: "javascript", + color: "tomato" + }, contributors: [ %{ avatar: Faker.Avatar.image_url(), From 35cf03b37d0d8ef2cc586fdc095cd8f240f33575 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 30 Sep 2018 09:05:55 +0800 Subject: [PATCH 023/129] refactor(comments): replace body:string -> body:text --- ...0005859_replace_string_to_text_in_post_comments.exs | 10 ++++++++++ ...30010331_replace_string_to_text_in_job_comments.exs | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 priv/repo/migrations/20180930005859_replace_string_to_text_in_post_comments.exs create mode 100644 priv/repo/migrations/20180930010331_replace_string_to_text_in_job_comments.exs diff --git a/priv/repo/migrations/20180930005859_replace_string_to_text_in_post_comments.exs b/priv/repo/migrations/20180930005859_replace_string_to_text_in_post_comments.exs new file mode 100644 index 000000000..11f902993 --- /dev/null +++ b/priv/repo/migrations/20180930005859_replace_string_to_text_in_post_comments.exs @@ -0,0 +1,10 @@ +defmodule MastaniServer.Repo.Migrations.ReplaceStringToTextInPostComments do + use Ecto.Migration + + def change do + alter table(:posts_comments) do + remove(:body) + add(:body, :text) + end + end +end diff --git a/priv/repo/migrations/20180930010331_replace_string_to_text_in_job_comments.exs b/priv/repo/migrations/20180930010331_replace_string_to_text_in_job_comments.exs new file mode 100644 index 000000000..c55c1d325 --- /dev/null +++ b/priv/repo/migrations/20180930010331_replace_string_to_text_in_job_comments.exs @@ -0,0 +1,10 @@ +defmodule MastaniServer.Repo.Migrations.ReplaceStringToTextInJobComments do + use Ecto.Migration + + def change do + alter table(:jobs_comments) do + remove(:body) + add(:body, :text) + end + end +end From d98d512a497835f2314050f4a97916e7563db4ea Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 30 Sep 2018 10:26:38 +0800 Subject: [PATCH 024/129] feat(video comments): basic curd comments --- .../cms/delegates/comment_curd.ex | 48 ++-- lib/mastani_server/cms/utils/matcher.ex | 19 +- lib/mastani_server/cms/video_comment.ex | 43 ++++ lib/mastani_server/cms/video_comment_reply.ex | 27 +++ .../20180930011202_create_video_comments.exs | 16 ++ ...0930012820_create_video_comments_reply.exs | 15 ++ ...80930014611_add_floor_to_video_comment.exs | 12 + ...80930021237_add_reply_to_video_comment.exs | 10 + ...930021707_drop_videos_comments_replies.exs | 7 + ...21722_recreate_videos_comments_replies.exs | 15 ++ .../mastani_server/cms/video_comment_test.exs | 221 ++++++++++++++++++ 11 files changed, 416 insertions(+), 17 deletions(-) create mode 100644 lib/mastani_server/cms/video_comment.ex create mode 100644 lib/mastani_server/cms/video_comment_reply.ex create mode 100644 priv/repo/migrations/20180930011202_create_video_comments.exs create mode 100644 priv/repo/migrations/20180930012820_create_video_comments_reply.exs create mode 100644 priv/repo/migrations/20180930014611_add_floor_to_video_comment.exs create mode 100644 priv/repo/migrations/20180930021237_add_reply_to_video_comment.exs create mode 100644 priv/repo/migrations/20180930021707_drop_videos_comments_replies.exs create mode 100644 priv/repo/migrations/20180930021722_recreate_videos_comments_replies.exs create mode 100644 test/mastani_server/cms/video_comment_test.exs diff --git a/lib/mastani_server/cms/delegates/comment_curd.ex b/lib/mastani_server/cms/delegates/comment_curd.ex index a0e637706..397dc536b 100644 --- a/lib/mastani_server/cms/delegates/comment_curd.ex +++ b/lib/mastani_server/cms/delegates/comment_curd.ex @@ -9,9 +9,10 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do import MastaniServer.CMS.Utils.Matcher import ShortMaps - alias MastaniServer.{Repo, Accounts} alias Helper.{ORM, QueryBuilder} - alias MastaniServer.CMS.{PostCommentReply, JobCommentReply} + alias MastaniServer.{Repo, Accounts} + + alias MastaniServer.CMS.{PostCommentReply, JobCommentReply, VideoCommentReply} alias Ecto.Multi @@ -36,9 +37,6 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do end end - defp merge_comment_attrs(:post, attrs, id), do: attrs |> Map.merge(%{post_id: id}) - defp merge_comment_attrs(:job, attrs, id), do: attrs |> Map.merge(%{job_id: id}) - @doc """ Delete the comment and increase all the floor after this comment """ @@ -103,6 +101,7 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do def reply_comment(thread, comment_id, body, %Accounts.User{id: user_id}) do with {:ok, action} <- match_action(thread, :comment), {:ok, comment} <- ORM.find(action.reactor, comment_id) do + next_floor = get_next_floor(thread, action.reactor, comment) attrs = %{ @@ -113,17 +112,13 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do } attrs = merge_reply_attrs(thread, attrs, comment) - brige_reply(thread, action.reactor, comment, attrs) + bridge_reply(thread, action.reactor, comment, attrs) end end - defp merge_reply_attrs(:post, attrs, comment), - do: attrs |> Map.merge(%{post_id: comment.post_id}) - - defp merge_reply_attrs(:job, attrs, comment), do: attrs |> Map.merge(%{job_id: comment.job_id}) - - defp brige_reply(:post, queryable, comment, attrs) do - # TODO: use Multi task to refactor + # simulate a join connection + # TODO: use Multi task to refactor + defp bridge_reply(:post, queryable, comment, attrs) do with {:ok, reply} <- ORM.create(queryable, attrs) do ORM.update(reply, %{reply_id: comment.id}) @@ -134,8 +129,7 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do end end - defp brige_reply(:job, queryable, comment, attrs) do - # TODO: use Multi task to refactor + defp bridge_reply(:job, queryable, comment, attrs) do with {:ok, reply} <- ORM.create(queryable, attrs) do ORM.update(reply, %{reply_id: comment.id}) @@ -145,6 +139,16 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do end end + defp bridge_reply(:video, queryable, comment, attrs) do + with {:ok, reply} <- ORM.create(queryable, attrs) do + ORM.update(reply, %{reply_id: comment.id}) + + {:ok, _} = VideoCommentReply |> ORM.create(%{video_comment_id: comment.id, reply_id: reply.id}) + + queryable |> ORM.find(reply.id) + end + end + # for create comment defp get_next_floor(thread, queryable, id) when is_integer(id) do dynamic = dynamic_comment_where(thread, id) @@ -163,9 +167,23 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do |> ORM.next_count() end + # merge_comment_attrs when create comemnt + defp merge_comment_attrs(:post, attrs, id), do: attrs |> Map.merge(%{post_id: id}) + defp merge_comment_attrs(:job, attrs, id), do: attrs |> Map.merge(%{job_id: id}) + defp merge_comment_attrs(:video, attrs, id), do: attrs |> Map.merge(%{video_id: id}) + + defp merge_reply_attrs(:post, attrs, comment), + do: attrs |> Map.merge(%{post_id: comment.post_id}) + + defp merge_reply_attrs(:job, attrs, comment), do: attrs |> Map.merge(%{job_id: comment.job_id}) + defp merge_reply_attrs(:video, attrs, comment), do: attrs |> Map.merge(%{video_id: comment.video_id}) + + defp dynamic_comment_where(:post, id), do: dynamic([c], c.post_id == ^id) defp dynamic_comment_where(:job, id), do: dynamic([c], c.job_id == ^id) + defp dynamic_comment_where(:video, id), do: dynamic([c], c.video_id == ^id) defp dynamic_reply_where(:post, comment), do: dynamic([c], c.post_id == ^comment.post_id) defp dynamic_reply_where(:job, comment), do: dynamic([c], c.job_id == ^comment.job_id) + defp dynamic_reply_where(:video, comment), do: dynamic([c], c.video_id == ^comment.video_id) end diff --git a/lib/mastani_server/cms/utils/matcher.ex b/lib/mastani_server/cms/utils/matcher.ex index be3b7e186..38e93f17f 100644 --- a/lib/mastani_server/cms/utils/matcher.ex +++ b/lib/mastani_server/cms/utils/matcher.ex @@ -6,20 +6,26 @@ defmodule MastaniServer.CMS.Utils.Matcher do alias MastaniServer.CMS.{ Community, + # threads Post, Video, Repo, Job, + # reactions PostFavorite, JobFavorite, PostStar, JobStar, + # comments PostComment, JobComment, - Tag, - Community, + VideoComment, + # commtnes reaction PostCommentLike, PostCommentDislike, + Tag, + Community, + # flags PostCommunityFlag, JobCommunityFlag, RepoCommunityFlag, @@ -88,6 +94,9 @@ defmodule MastaniServer.CMS.Utils.Matcher do def match_action(:video, :community), do: {:ok, %{target: Video, reactor: Community, flag: VideoCommunityFlag}} + def match_action(:video, :comment), + do: {:ok, %{target: Video, reactor: VideoComment, preload: :author}} + ######################################### ## repos ... ######################################### @@ -110,6 +119,12 @@ defmodule MastaniServer.CMS.Utils.Matcher do :job_comment -> {:ok, dynamic([p], p.job_comment_id == ^id)} + :video -> + {:ok, dynamic([p], p.video_id == ^id)} + + :video_comment -> + {:ok, dynamic([p], p.video_comment_id == ^id)} + _ -> {:error, 'where is not match'} end diff --git a/lib/mastani_server/cms/video_comment.ex b/lib/mastani_server/cms/video_comment.ex new file mode 100644 index 000000000..e1d87985d --- /dev/null +++ b/lib/mastani_server/cms/video_comment.ex @@ -0,0 +1,43 @@ +defmodule MastaniServer.CMS.VideoComment do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + + alias MastaniServer.CMS.{ + Video, + # PostCommentDislike, + # PostCommentLike, + VideoCommentReply + } + + @required_fields ~w(body author_id video_id floor)a + @optional_fields ~w(reply_id)a + + @type t :: %VideoComment{} + schema "videos_comments" do + field(:body, :string) + field(:floor, :integer) + belongs_to(:author, Accounts.User, foreign_key: :author_id) + belongs_to(:video, Video, foreign_key: :video_id) + belongs_to(:reply_to, VideoComment, foreign_key: :reply_id) + + has_many(:replies, {"videos_comments_replies", VideoCommentReply}) + # has_many(:likes, {"posts_comments_likes", PostCommentLike}) + # has_many(:dislikes, {"posts_comments_dislikes", PostCommentDislike}) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%VideoComment{} = video_comment, attrs) do + video_comment + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + |> validate_length(:body, min: 1) + |> foreign_key_constraint(:video_id) + |> foreign_key_constraint(:author_id) + end +end diff --git a/lib/mastani_server/cms/video_comment_reply.ex b/lib/mastani_server/cms/video_comment_reply.ex new file mode 100644 index 000000000..0b68fc4b8 --- /dev/null +++ b/lib/mastani_server/cms/video_comment_reply.ex @@ -0,0 +1,27 @@ +defmodule MastaniServer.CMS.VideoCommentReply do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.CMS.VideoComment + + @required_fields ~w(video_comment_id reply_id)a + + @type t :: %VideoCommentReply{} + schema "videos_comments_replies" do + belongs_to(:video_comment, VideoComment, foreign_key: :video_comment_id) + belongs_to(:reply, VideoComment, foreign_key: :reply_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%VideoCommentReply{} = video_comment_reply, attrs) do + video_comment_reply + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:video_comment_id) + |> foreign_key_constraint(:reply_id) + end +end diff --git a/priv/repo/migrations/20180930011202_create_video_comments.exs b/priv/repo/migrations/20180930011202_create_video_comments.exs new file mode 100644 index 000000000..85be724aa --- /dev/null +++ b/priv/repo/migrations/20180930011202_create_video_comments.exs @@ -0,0 +1,16 @@ +defmodule MastaniServer.Repo.Migrations.CreateVideoComments do + use Ecto.Migration + + def change do + create table(:videos_comments) do + add(:body, :text) + add(:author_id, references(:users, on_delete: :delete_all), null: false) + add(:video_id, references(:cms_videos, on_delete: :delete_all), null: false) + + timestamps() + end + + create(index(:videos_comments, [:author_id])) + create(index(:videos_comments, [:video_id])) + end +end diff --git a/priv/repo/migrations/20180930012820_create_video_comments_reply.exs b/priv/repo/migrations/20180930012820_create_video_comments_reply.exs new file mode 100644 index 000000000..d21fb5194 --- /dev/null +++ b/priv/repo/migrations/20180930012820_create_video_comments_reply.exs @@ -0,0 +1,15 @@ +defmodule MastaniServer.Repo.Migrations.CreateVideoCommentsReply do + use Ecto.Migration + + def change do + create table(:videos_comments_replies) do + add(:comment_id, references(:videos_comments, on_delete: :delete_all), null: false) + add(:reply_id, references(:videos_comments, on_delete: :delete_all), null: false) + + timestamps() + end + + create(index(:videos_comments_replies, [:comment_id])) + create(index(:videos_comments_replies, [:reply_id])) + end +end diff --git a/priv/repo/migrations/20180930014611_add_floor_to_video_comment.exs b/priv/repo/migrations/20180930014611_add_floor_to_video_comment.exs new file mode 100644 index 000000000..e4f2be474 --- /dev/null +++ b/priv/repo/migrations/20180930014611_add_floor_to_video_comment.exs @@ -0,0 +1,12 @@ +defmodule MastaniServer.Repo.Migrations.AddFloorToVideoComment do + use Ecto.Migration + + def change do + alter table(:videos_comments) do + add(:floor, :integer, default: 0) + end + + create(index(:videos_comments, [:floor])) + create(unique_index(:videos_comments, [:video_id, :author_id, :floor])) + end +end diff --git a/priv/repo/migrations/20180930021237_add_reply_to_video_comment.exs b/priv/repo/migrations/20180930021237_add_reply_to_video_comment.exs new file mode 100644 index 000000000..b5d6bfe9e --- /dev/null +++ b/priv/repo/migrations/20180930021237_add_reply_to_video_comment.exs @@ -0,0 +1,10 @@ +defmodule MastaniServer.Repo.Migrations.AddReplyToVideoComment do + use Ecto.Migration + + def change do + alter table(:videos_comments) do + # add(:author_id, references(:users, on_delete: :delete_all), null: false) + add(:reply_id, references(:videos_comments, on_delete: :delete_all)) + end + end +end diff --git a/priv/repo/migrations/20180930021707_drop_videos_comments_replies.exs b/priv/repo/migrations/20180930021707_drop_videos_comments_replies.exs new file mode 100644 index 000000000..1ea55ae25 --- /dev/null +++ b/priv/repo/migrations/20180930021707_drop_videos_comments_replies.exs @@ -0,0 +1,7 @@ +defmodule MastaniServer.Repo.Migrations.DropVideosCommentsReplies do + use Ecto.Migration + + def change do + drop(table(:videos_comments_replies)) + end +end diff --git a/priv/repo/migrations/20180930021722_recreate_videos_comments_replies.exs b/priv/repo/migrations/20180930021722_recreate_videos_comments_replies.exs new file mode 100644 index 000000000..79c46fef9 --- /dev/null +++ b/priv/repo/migrations/20180930021722_recreate_videos_comments_replies.exs @@ -0,0 +1,15 @@ +defmodule MastaniServer.Repo.Migrations.RecreateVideosCommentsReplies do + use Ecto.Migration + + def change do + create table(:videos_comments_replies) do + add(:video_comment_id, references(:videos_comments, on_delete: :delete_all), null: false) + add(:reply_id, references(:videos_comments, on_delete: :delete_all), null: false) + + timestamps() + end + + create(index(:videos_comments_replies, [:video_comment_id])) + create(index(:videos_comments_replies, [:reply_id])) + end +end diff --git a/test/mastani_server/cms/video_comment_test.exs b/test/mastani_server/cms/video_comment_test.exs new file mode 100644 index 000000000..8a48aec26 --- /dev/null +++ b/test/mastani_server/cms/video_comment_test.exs @@ -0,0 +1,221 @@ +defmodule MastaniServer.Test.VideoComment do + # currently only test comments for video type, rename and seprherate later + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.CMS + + alias CMS.{VideoComment, VideoCommentReply} + + setup do + {:ok, video} = db_insert(:video) + {:ok, user} = db_insert(:user) + + body = "this is a test comment" + + {:ok, comment} = CMS.create_comment(:video, video.id, body, user) + + {:ok, ~m(video user comment)a} + end + + describe "[comment CURD]" do + @tag :wip + test "login user comment to exsiting video", ~m(video user)a do + body = "this is a test comment" + + assert {:ok, comment} = CMS.create_comment(:video, video.id, body, user) + + assert comment.video_id == video.id + assert comment.body == body + assert comment.author_id == user.id + end + + @tag :wip + test "created comment should have a increased floor number", ~m(video user)a do + body = "this is a test comment" + + assert {:ok, comment1} = CMS.create_comment(:video, video.id, body, user) + + {:ok, user2} = db_insert(:user) + + assert {:ok, comment2} = CMS.create_comment(:video, video.id, body, user2) + + assert comment1.floor == 2 + assert comment2.floor == 3 + end + + @tag :wip + test "create comment to non-exsit video fails", ~m(user)a do + body = "this is a test comment" + + assert {:error, _} = CMS.create_comment(:video, non_exsit_id(), body, user) + end + + @tag :wip + test "can reply a comment, and reply should be in comment replies list", ~m(comment user)a do + reply_body = "this is a reply comment" + + {:ok, reply} = CMS.reply_comment(:video, comment.id, reply_body, user) + + {:ok, reply_preload} = ORM.find(VideoComment, reply.id, preload: :reply_to) + {:ok, comment_preload} = ORM.find(VideoComment, comment.id, preload: :replies) + + assert reply_preload.reply_to.id == comment.id + assert reply_preload.author_id == user.id + assert reply_preload.body == reply_body + # reply id should be in comments replies list + assert comment_preload.replies |> Enum.any?(&(&1.reply_id == reply.id)) + end + + @tag :wip + test "comment can be deleted", ~m(video user)a do + body = "this is a test comment" + + assert {:ok, comment} = CMS.create_comment(:video, video.id, body, user) + + {:ok, deleted} = CMS.delete_comment(:video, comment.id) + assert deleted.id == comment.id + end + + @tag :wip + test "after delete, the coments of id > deleted.id should decrease the floor number", + ~m(video user)a do + body = "this is a test comment" + # in setup we have a comment + total = 30 + 1 + + comments = + Enum.reduce(1..total, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:video, video.id, body, user) + + acc ++ [value] + end) + + [comment_1, comment_2, comment_3, comment_last] = comments |> firstn_and_last(3) + + assert comment_1.floor == 2 + assert comment_2.floor == 3 + assert comment_3.floor == 4 + assert comment_last.floor == total + 1 + + {:ok, _} = CMS.delete_comment(:video, comment_1.id) + + {:ok, new_comment_2} = ORM.find(VideoComment, comment_2.id) + {:ok, new_comment_3} = ORM.find(VideoComment, comment_3.id) + {:ok, new_comment_last} = ORM.find(VideoComment, comment_last.id) + + assert new_comment_2.floor == 2 + assert new_comment_3.floor == 3 + assert new_comment_last.floor == total + end + + @tag :wip + test "comment with replies should be deleted together", ~m(comment user)a do + reply_body = "this is a reply comment" + + {:ok, reply} = CMS.reply_comment(:video, comment.id, reply_body, user) + + VideoComment |> ORM.find_delete(comment.id) + + {:error, _} = ORM.find(VideoComment, comment.id) + {:error, _} = ORM.find(VideoComment, reply.id) + + {:error, _} = + VideoCommentReply |> ORM.find_by(video_comment_id: comment.id, reply_id: reply.id) + end + + @tag :wip + test "comments pagination should work", ~m(video user)a do + body = "fake comment" + + Enum.reduce(1..30, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:video, video.id, body, user) + + acc ++ [value] + end) + + {:ok, results} = CMS.list_comments(:video, video.id, %{page: 1, size: 10}) + + assert results |> is_valid_pagination?(:raw) + end + + @tag :wip + test "comment reply can be list one-by-one --> by replied user", ~m(comment)a do + {:ok, user1} = db_insert(:user) + {:ok, user2} = db_insert(:user) + {:ok, user3} = db_insert(:user) + + {:ok, _} = CMS.reply_comment(:video, comment.id, "reply by user1", user1) + + {:ok, _} = CMS.reply_comment(:video, comment.id, "reply by user2", user2) + + {:ok, _} = CMS.reply_comment(:video, comment.id, "reply by user3", user3) + + {:ok, found_reply1} = CMS.list_replies(:video, comment.id, user1) + assert user1.id == found_reply1 |> List.first() |> Map.get(:author_id) + + {:ok, found_reply2} = CMS.list_replies(:video, comment.id, user2) + assert user2.id == found_reply2 |> List.first() |> Map.get(:author_id) + + {:ok, found_reply3} = CMS.list_replies(:video, comment.id, user3) + assert user3.id == found_reply3 |> List.first() |> Map.get(:author_id) + end + end + + # describe "[comment Reactions]" do + # test "user can like a comment", ~m(comment user)a do + # {:ok, liked_comment} = CMS.like_comment(:post_comment, comment.id, user) + + # {:ok, comment_preload} = ORM.find(PostComment, liked_comment.id, preload: :likes) + + # assert comment_preload.likes |> Enum.any?(&(&1.post_comment_id == comment.id)) + # end + + # test "user like comment twice fails", ~m(comment user)a do + # {:ok, _} = CMS.like_comment(:post_comment, comment.id, user) + # {:error, _error} = CMS.like_comment(:post_comment, comment.id, user) + # # TODO: fix err_msg later + # end + + # test "user can undo a like action", ~m(comment user)a do + # {:ok, like} = CMS.like_comment(:post_comment, comment.id, user) + # {:ok, _} = CMS.undo_like_comment(:post_comment, comment.id, user) + + # {:ok, comment_preload} = ORM.find(PostComment, comment.id, preload: :likes) + # assert false == comment_preload.likes |> Enum.any?(&(&1.id == like.id)) + # end + + # test "user can dislike a comment", ~m(comment user)a do + # # {:ok, like} = CMS.reaction(:post_comment, :like, comment.id, user.id) + # {:ok, disliked_comment} = CMS.dislike_comment(:post_comment, comment.id, user) + + # {:ok, comment_preload} = ORM.find(PostComment, disliked_comment.id, preload: :dislikes) + + # assert comment_preload.dislikes |> Enum.any?(&(&1.post_comment_id == comment.id)) + # end + + # test "user can undo a dislike action", ~m(comment user)a do + # {:ok, dislike} = CMS.dislike_comment(:post_comment, comment.id, user) + # {:ok, _} = CMS.undo_dislike_comment(:post_comment, comment.id, user) + + # {:ok, comment_preload} = ORM.find(PostComment, comment.id, preload: :dislikes) + # assert false == comment_preload.dislikes |> Enum.any?(&(&1.id == dislike.id)) + # end + + # test "user can get paged likes of a post comment", ~m(comment)a do + # {:ok, user1} = db_insert(:user) + # {:ok, user2} = db_insert(:user) + # {:ok, user3} = db_insert(:user) + + # {:ok, _like1} = CMS.like_comment(:post_comment, comment.id, user1) + # {:ok, _like2} = CMS.like_comment(:post_comment, comment.id, user2) + # {:ok, _like3} = CMS.like_comment(:post_comment, comment.id, user3) + + # {:ok, results} = CMS.reaction_users(:post_comment, :like, comment.id, %{page: 1, size: 10}) + + # assert results.entries |> Enum.any?(&(&1.id == user1.id)) + # assert results.entries |> Enum.any?(&(&1.id == user2.id)) + # assert results.entries |> Enum.any?(&(&1.id == user3.id)) + # end + # end +end From 94cbaec85ab3e6ea6a038926484b7759b453e633 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 30 Sep 2018 11:00:24 +0800 Subject: [PATCH 025/129] feat(video comments): add like/dilike --- .../cms/delegates/comment_curd.ex | 7 +- .../cms/delegates/comment_reaction.ex | 11 ++- lib/mastani_server/cms/utils/matcher.ex | 11 ++- lib/mastani_server/cms/video_comment.ex | 8 +- .../cms/video_comment_dislike.ex | 29 ++++++ lib/mastani_server/cms/video_comment_like.ex | 29 ++++++ ...30023126_create_likes_to_video_comment.exs | 14 +++ ...23322_create_dislikes_to_video_comment.exs | 14 +++ .../mastani_server/cms/video_comment_test.exs | 96 +++++++++---------- 9 files changed, 156 insertions(+), 63 deletions(-) create mode 100644 lib/mastani_server/cms/video_comment_dislike.ex create mode 100644 lib/mastani_server/cms/video_comment_like.ex create mode 100644 priv/repo/migrations/20180930023126_create_likes_to_video_comment.exs create mode 100644 priv/repo/migrations/20180930023322_create_dislikes_to_video_comment.exs diff --git a/lib/mastani_server/cms/delegates/comment_curd.ex b/lib/mastani_server/cms/delegates/comment_curd.ex index 397dc536b..b480b1f50 100644 --- a/lib/mastani_server/cms/delegates/comment_curd.ex +++ b/lib/mastani_server/cms/delegates/comment_curd.ex @@ -101,7 +101,6 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do def reply_comment(thread, comment_id, body, %Accounts.User{id: user_id}) do with {:ok, action} <- match_action(thread, :comment), {:ok, comment} <- ORM.find(action.reactor, comment_id) do - next_floor = get_next_floor(thread, action.reactor, comment) attrs = %{ @@ -143,7 +142,8 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do with {:ok, reply} <- ORM.create(queryable, attrs) do ORM.update(reply, %{reply_id: comment.id}) - {:ok, _} = VideoCommentReply |> ORM.create(%{video_comment_id: comment.id, reply_id: reply.id}) + {:ok, _} = + VideoCommentReply |> ORM.create(%{video_comment_id: comment.id, reply_id: reply.id}) queryable |> ORM.find(reply.id) end @@ -176,8 +176,9 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do do: attrs |> Map.merge(%{post_id: comment.post_id}) defp merge_reply_attrs(:job, attrs, comment), do: attrs |> Map.merge(%{job_id: comment.job_id}) - defp merge_reply_attrs(:video, attrs, comment), do: attrs |> Map.merge(%{video_id: comment.video_id}) + defp merge_reply_attrs(:video, attrs, comment), + do: attrs |> Map.merge(%{video_id: comment.video_id}) defp dynamic_comment_where(:post, id), do: dynamic([c], c.post_id == ^id) defp dynamic_comment_where(:job, id), do: dynamic([c], c.job_id == ^id) diff --git a/lib/mastani_server/cms/delegates/comment_reaction.ex b/lib/mastani_server/cms/delegates/comment_reaction.ex index 8c68a97a5..5b0745ba9 100644 --- a/lib/mastani_server/cms/delegates/comment_reaction.ex +++ b/lib/mastani_server/cms/delegates/comment_reaction.ex @@ -1,8 +1,8 @@ defmodule MastaniServer.CMS.Delegate.CommentReaction do import MastaniServer.CMS.Utils.Matcher - alias MastaniServer.Accounts alias Helper.ORM + alias MastaniServer.Accounts def like_comment(thread, comment_id, %Accounts.User{id: user_id}) do feel_comment(thread, comment_id, user_id, :like) @@ -20,10 +20,14 @@ defmodule MastaniServer.CMS.Delegate.CommentReaction do undo_feel_comment(thread, comment_id, user_id, :dislike) end + defp merge_thread_comment_id(:post_comment, comment_id), do: %{post_comment_id: comment_id} + defp merge_thread_comment_id(:video_comment, comment_id), do: %{video_comment_id: comment_id} + defp feel_comment(thread, comment_id, user_id, feeling) when valid_feeling(feeling) do with {:ok, action} <- match_action(thread, feeling) do - clause = %{post_comment_id: comment_id, user_id: user_id} + clause = Map.merge(%{user_id: user_id}, merge_thread_comment_id(thread, comment_id)) + # clause = %{post_comment_id: comment_id, user_id: user_id} case ORM.find_by(action.reactor, clause) do {:ok, _} -> @@ -39,7 +43,8 @@ defmodule MastaniServer.CMS.Delegate.CommentReaction do defp undo_feel_comment(thread, comment_id, user_id, feeling) do with {:ok, action} <- match_action(thread, feeling) do - clause = %{post_comment_id: comment_id, user_id: user_id} + clause = Map.merge(%{user_id: user_id}, merge_thread_comment_id(thread, comment_id)) + # clause = %{post_comment_id: comment_id, user_id: user_id} ORM.findby_delete(action.reactor, clause) ORM.find(action.target, comment_id) end diff --git a/lib/mastani_server/cms/utils/matcher.ex b/lib/mastani_server/cms/utils/matcher.ex index 38e93f17f..e50b63d08 100644 --- a/lib/mastani_server/cms/utils/matcher.ex +++ b/lib/mastani_server/cms/utils/matcher.ex @@ -23,6 +23,9 @@ defmodule MastaniServer.CMS.Utils.Matcher do # commtnes reaction PostCommentLike, PostCommentDislike, + VideoCommentLike, + VideoCommentDislike, + # Tag, Community, # flags @@ -95,7 +98,13 @@ defmodule MastaniServer.CMS.Utils.Matcher do do: {:ok, %{target: Video, reactor: Community, flag: VideoCommunityFlag}} def match_action(:video, :comment), - do: {:ok, %{target: Video, reactor: VideoComment, preload: :author}} + do: {:ok, %{target: Video, reactor: VideoComment, preload: :author}} + + def match_action(:video_comment, :like), + do: {:ok, %{target: VideoComment, reactor: VideoCommentLike}} + + def match_action(:video_comment, :dislike), + do: {:ok, %{target: VideoComment, reactor: VideoCommentDislike}} ######################################### ## repos ... diff --git a/lib/mastani_server/cms/video_comment.ex b/lib/mastani_server/cms/video_comment.ex index e1d87985d..f7dd9a36c 100644 --- a/lib/mastani_server/cms/video_comment.ex +++ b/lib/mastani_server/cms/video_comment.ex @@ -8,8 +8,8 @@ defmodule MastaniServer.CMS.VideoComment do alias MastaniServer.CMS.{ Video, - # PostCommentDislike, - # PostCommentLike, + VideoCommentDislike, + VideoCommentLike, VideoCommentReply } @@ -25,8 +25,8 @@ defmodule MastaniServer.CMS.VideoComment do belongs_to(:reply_to, VideoComment, foreign_key: :reply_id) has_many(:replies, {"videos_comments_replies", VideoCommentReply}) - # has_many(:likes, {"posts_comments_likes", PostCommentLike}) - # has_many(:dislikes, {"posts_comments_dislikes", PostCommentDislike}) + has_many(:likes, {"videos_comments_likes", VideoCommentLike}) + has_many(:dislikes, {"videos_comments_dislikes", VideoCommentDislike}) timestamps(type: :utc_datetime) end diff --git a/lib/mastani_server/cms/video_comment_dislike.ex b/lib/mastani_server/cms/video_comment_dislike.ex new file mode 100644 index 000000000..9113dcbf1 --- /dev/null +++ b/lib/mastani_server/cms/video_comment_dislike.ex @@ -0,0 +1,29 @@ +defmodule MastaniServer.CMS.VideoCommentDislike do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.VideoComment + + @required_fields ~w(video_comment_id user_id)a + + @type t :: %VideoCommentDislike{} + schema "videos_comments_dislikes" do + belongs_to(:user, Accounts.User, foreign_key: :user_id) + belongs_to(:video_comment, VideoComment, foreign_key: :video_comment_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%VideoCommentDislike{} = video_comment_dislike, attrs) do + video_comment_dislike + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:video_comment_id) + |> foreign_key_constraint(:user_id) + |> unique_constraint(:user_id, name: :videos_comments_dislikes_user_id_video_comment_id_index) + end +end diff --git a/lib/mastani_server/cms/video_comment_like.ex b/lib/mastani_server/cms/video_comment_like.ex new file mode 100644 index 000000000..6c1ba97eb --- /dev/null +++ b/lib/mastani_server/cms/video_comment_like.ex @@ -0,0 +1,29 @@ +defmodule MastaniServer.CMS.VideoCommentLike do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.VideoComment + + @required_fields ~w(video_comment_id user_id)a + + @type t :: %VideoCommentLike{} + schema "videos_comments_likes" do + belongs_to(:user, Accounts.User, foreign_key: :user_id) + belongs_to(:video_comment, VideoComment, foreign_key: :video_comment_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%VideoCommentLike{} = video_comment_like, attrs) do + video_comment_like + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:video_comment_id) + |> foreign_key_constraint(:user_id) + |> unique_constraint(:user_id, name: :videos_comments_likes_user_id_video_comment_id_index) + end +end diff --git a/priv/repo/migrations/20180930023126_create_likes_to_video_comment.exs b/priv/repo/migrations/20180930023126_create_likes_to_video_comment.exs new file mode 100644 index 000000000..47aeb9e5a --- /dev/null +++ b/priv/repo/migrations/20180930023126_create_likes_to_video_comment.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreateLikesToVideoComment do + use Ecto.Migration + + def change do + create table(:videos_comments_likes) do + add(:user_id, references(:users, on_delete: :delete_all), null: false) + add(:video_comment_id, references(:videos_comments, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:videos_comments_likes, [:user_id, :video_comment_id])) + end +end diff --git a/priv/repo/migrations/20180930023322_create_dislikes_to_video_comment.exs b/priv/repo/migrations/20180930023322_create_dislikes_to_video_comment.exs new file mode 100644 index 000000000..fa42b6828 --- /dev/null +++ b/priv/repo/migrations/20180930023322_create_dislikes_to_video_comment.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreateDislikesToVideoComment do + use Ecto.Migration + + def change do + create table(:videos_comments_dislikes) do + add(:user_id, references(:users, on_delete: :delete_all), null: false) + add(:video_comment_id, references(:videos_comments, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:videos_comments_dislikes, [:user_id, :video_comment_id])) + end +end diff --git a/test/mastani_server/cms/video_comment_test.exs b/test/mastani_server/cms/video_comment_test.exs index 8a48aec26..e627ea825 100644 --- a/test/mastani_server/cms/video_comment_test.exs +++ b/test/mastani_server/cms/video_comment_test.exs @@ -19,7 +19,6 @@ defmodule MastaniServer.Test.VideoComment do end describe "[comment CURD]" do - @tag :wip test "login user comment to exsiting video", ~m(video user)a do body = "this is a test comment" @@ -30,7 +29,6 @@ defmodule MastaniServer.Test.VideoComment do assert comment.author_id == user.id end - @tag :wip test "created comment should have a increased floor number", ~m(video user)a do body = "this is a test comment" @@ -44,14 +42,12 @@ defmodule MastaniServer.Test.VideoComment do assert comment2.floor == 3 end - @tag :wip test "create comment to non-exsit video fails", ~m(user)a do body = "this is a test comment" assert {:error, _} = CMS.create_comment(:video, non_exsit_id(), body, user) end - @tag :wip test "can reply a comment, and reply should be in comment replies list", ~m(comment user)a do reply_body = "this is a reply comment" @@ -67,7 +63,6 @@ defmodule MastaniServer.Test.VideoComment do assert comment_preload.replies |> Enum.any?(&(&1.reply_id == reply.id)) end - @tag :wip test "comment can be deleted", ~m(video user)a do body = "this is a test comment" @@ -77,9 +72,9 @@ defmodule MastaniServer.Test.VideoComment do assert deleted.id == comment.id end - @tag :wip + # TODO it's bug test "after delete, the coments of id > deleted.id should decrease the floor number", - ~m(video user)a do + ~m(video user)a do body = "this is a test comment" # in setup we have a comment total = 30 + 1 @@ -109,7 +104,6 @@ defmodule MastaniServer.Test.VideoComment do assert new_comment_last.floor == total end - @tag :wip test "comment with replies should be deleted together", ~m(comment user)a do reply_body = "this is a reply comment" @@ -124,7 +118,6 @@ defmodule MastaniServer.Test.VideoComment do VideoCommentReply |> ORM.find_by(video_comment_id: comment.id, reply_id: reply.id) end - @tag :wip test "comments pagination should work", ~m(video user)a do body = "fake comment" @@ -139,7 +132,6 @@ defmodule MastaniServer.Test.VideoComment do assert results |> is_valid_pagination?(:raw) end - @tag :wip test "comment reply can be list one-by-one --> by replied user", ~m(comment)a do {:ok, user1} = db_insert(:user) {:ok, user2} = db_insert(:user) @@ -162,60 +154,60 @@ defmodule MastaniServer.Test.VideoComment do end end - # describe "[comment Reactions]" do - # test "user can like a comment", ~m(comment user)a do - # {:ok, liked_comment} = CMS.like_comment(:post_comment, comment.id, user) + describe "[comment Reactions]" do + test "user can like a comment", ~m(comment user)a do + {:ok, liked_comment} = CMS.like_comment(:video_comment, comment.id, user) - # {:ok, comment_preload} = ORM.find(PostComment, liked_comment.id, preload: :likes) + {:ok, comment_preload} = ORM.find(VideoComment, liked_comment.id, preload: :likes) - # assert comment_preload.likes |> Enum.any?(&(&1.post_comment_id == comment.id)) - # end + assert comment_preload.likes |> Enum.any?(&(&1.video_comment_id == comment.id)) + end - # test "user like comment twice fails", ~m(comment user)a do - # {:ok, _} = CMS.like_comment(:post_comment, comment.id, user) - # {:error, _error} = CMS.like_comment(:post_comment, comment.id, user) - # # TODO: fix err_msg later - # end + test "user like comment twice fails", ~m(comment user)a do + {:ok, _} = CMS.like_comment(:video_comment, comment.id, user) + {:error, _error} = CMS.like_comment(:video_comment, comment.id, user) + # TODO: fix err_msg later + end - # test "user can undo a like action", ~m(comment user)a do - # {:ok, like} = CMS.like_comment(:post_comment, comment.id, user) - # {:ok, _} = CMS.undo_like_comment(:post_comment, comment.id, user) + test "user can undo a like action", ~m(comment user)a do + {:ok, like} = CMS.like_comment(:video_comment, comment.id, user) + {:ok, _} = CMS.undo_like_comment(:video_comment, comment.id, user) - # {:ok, comment_preload} = ORM.find(PostComment, comment.id, preload: :likes) - # assert false == comment_preload.likes |> Enum.any?(&(&1.id == like.id)) - # end + {:ok, comment_preload} = ORM.find(VideoComment, comment.id, preload: :likes) + assert false == comment_preload.likes |> Enum.any?(&(&1.id == like.id)) + end - # test "user can dislike a comment", ~m(comment user)a do - # # {:ok, like} = CMS.reaction(:post_comment, :like, comment.id, user.id) - # {:ok, disliked_comment} = CMS.dislike_comment(:post_comment, comment.id, user) + test "user can dislike a comment", ~m(comment user)a do + # {:ok, like} = CMS.reaction(:video_comment, :like, comment.id, user.id) + {:ok, disliked_comment} = CMS.dislike_comment(:video_comment, comment.id, user) - # {:ok, comment_preload} = ORM.find(PostComment, disliked_comment.id, preload: :dislikes) + {:ok, comment_preload} = ORM.find(VideoComment, disliked_comment.id, preload: :dislikes) - # assert comment_preload.dislikes |> Enum.any?(&(&1.post_comment_id == comment.id)) - # end + assert comment_preload.dislikes |> Enum.any?(&(&1.video_comment_id == comment.id)) + end - # test "user can undo a dislike action", ~m(comment user)a do - # {:ok, dislike} = CMS.dislike_comment(:post_comment, comment.id, user) - # {:ok, _} = CMS.undo_dislike_comment(:post_comment, comment.id, user) + test "user can undo a dislike action", ~m(comment user)a do + {:ok, dislike} = CMS.dislike_comment(:video_comment, comment.id, user) + {:ok, _} = CMS.undo_dislike_comment(:video_comment, comment.id, user) - # {:ok, comment_preload} = ORM.find(PostComment, comment.id, preload: :dislikes) - # assert false == comment_preload.dislikes |> Enum.any?(&(&1.id == dislike.id)) - # end + {:ok, comment_preload} = ORM.find(VideoComment, comment.id, preload: :dislikes) + assert false == comment_preload.dislikes |> Enum.any?(&(&1.id == dislike.id)) + end - # test "user can get paged likes of a post comment", ~m(comment)a do - # {:ok, user1} = db_insert(:user) - # {:ok, user2} = db_insert(:user) - # {:ok, user3} = db_insert(:user) + test "user can get paged likes of a video comment", ~m(comment)a do + {:ok, user1} = db_insert(:user) + {:ok, user2} = db_insert(:user) + {:ok, user3} = db_insert(:user) - # {:ok, _like1} = CMS.like_comment(:post_comment, comment.id, user1) - # {:ok, _like2} = CMS.like_comment(:post_comment, comment.id, user2) - # {:ok, _like3} = CMS.like_comment(:post_comment, comment.id, user3) + {:ok, _like1} = CMS.like_comment(:video_comment, comment.id, user1) + {:ok, _like2} = CMS.like_comment(:video_comment, comment.id, user2) + {:ok, _like3} = CMS.like_comment(:video_comment, comment.id, user3) - # {:ok, results} = CMS.reaction_users(:post_comment, :like, comment.id, %{page: 1, size: 10}) + {:ok, results} = CMS.reaction_users(:video_comment, :like, comment.id, %{page: 1, size: 10}) - # assert results.entries |> Enum.any?(&(&1.id == user1.id)) - # assert results.entries |> Enum.any?(&(&1.id == user2.id)) - # assert results.entries |> Enum.any?(&(&1.id == user3.id)) - # end - # end + assert results.entries |> Enum.any?(&(&1.id == user1.id)) + assert results.entries |> Enum.any?(&(&1.id == user2.id)) + assert results.entries |> Enum.any?(&(&1.id == user3.id)) + end + end end From ae7c30b340567e2af5ccef8f22dbf38264640de9 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 30 Sep 2018 12:24:04 +0800 Subject: [PATCH 026/129] feat(repo comment test): done with test cover --- .../cms/delegates/comment_curd.ex | 20 +- .../cms/delegates/comment_reaction.ex | 1 + lib/mastani_server/cms/repo_comment.ex | 43 ++++ .../cms/repo_comment_dislike.ex | 29 +++ lib/mastani_server/cms/repo_comment_like.ex | 29 +++ lib/mastani_server/cms/repo_comment_reply.ex | 27 +++ lib/mastani_server/cms/utils/matcher.ex | 19 ++ .../20180930030608_create_repo_comment.exs | 19 ++ ...180930031135_create_repo_comment_reply.exs | 15 ++ ...180930041418_create_repo_comment_likes.exs | 14 ++ ...930041550_create_repo_comment_dislikes.exs | 14 ++ test/mastani_server/cms/repo_comment_test.exs | 211 ++++++++++++++++++ 12 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 lib/mastani_server/cms/repo_comment.ex create mode 100644 lib/mastani_server/cms/repo_comment_dislike.ex create mode 100644 lib/mastani_server/cms/repo_comment_like.ex create mode 100644 lib/mastani_server/cms/repo_comment_reply.ex create mode 100644 priv/repo/migrations/20180930030608_create_repo_comment.exs create mode 100644 priv/repo/migrations/20180930031135_create_repo_comment_reply.exs create mode 100644 priv/repo/migrations/20180930041418_create_repo_comment_likes.exs create mode 100644 priv/repo/migrations/20180930041550_create_repo_comment_dislikes.exs create mode 100644 test/mastani_server/cms/repo_comment_test.exs diff --git a/lib/mastani_server/cms/delegates/comment_curd.ex b/lib/mastani_server/cms/delegates/comment_curd.ex index b480b1f50..e1e9aa737 100644 --- a/lib/mastani_server/cms/delegates/comment_curd.ex +++ b/lib/mastani_server/cms/delegates/comment_curd.ex @@ -12,7 +12,7 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do alias Helper.{ORM, QueryBuilder} alias MastaniServer.{Repo, Accounts} - alias MastaniServer.CMS.{PostCommentReply, JobCommentReply, VideoCommentReply} + alias MastaniServer.CMS.{PostCommentReply, JobCommentReply, VideoCommentReply, RepoCommentReply} alias Ecto.Multi @@ -117,6 +117,7 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do # simulate a join connection # TODO: use Multi task to refactor + # TODO: refactor boilerplate code defp bridge_reply(:post, queryable, comment, attrs) do with {:ok, reply} <- ORM.create(queryable, attrs) do ORM.update(reply, %{reply_id: comment.id}) @@ -149,6 +150,17 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do end end + defp bridge_reply(:repo, queryable, comment, attrs) do + with {:ok, reply} <- ORM.create(queryable, attrs) do + ORM.update(reply, %{reply_id: comment.id}) + + {:ok, _} = + RepoCommentReply |> ORM.create(%{repo_comment_id: comment.id, reply_id: reply.id}) + + queryable |> ORM.find(reply.id) + end + end + # for create comment defp get_next_floor(thread, queryable, id) when is_integer(id) do dynamic = dynamic_comment_where(thread, id) @@ -171,6 +183,7 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do defp merge_comment_attrs(:post, attrs, id), do: attrs |> Map.merge(%{post_id: id}) defp merge_comment_attrs(:job, attrs, id), do: attrs |> Map.merge(%{job_id: id}) defp merge_comment_attrs(:video, attrs, id), do: attrs |> Map.merge(%{video_id: id}) + defp merge_comment_attrs(:repo, attrs, id), do: attrs |> Map.merge(%{repo_id: id}) defp merge_reply_attrs(:post, attrs, comment), do: attrs |> Map.merge(%{post_id: comment.post_id}) @@ -180,11 +193,16 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do defp merge_reply_attrs(:video, attrs, comment), do: attrs |> Map.merge(%{video_id: comment.video_id}) + defp merge_reply_attrs(:repo, attrs, comment), + do: attrs |> Map.merge(%{repo_id: comment.repo_id}) + defp dynamic_comment_where(:post, id), do: dynamic([c], c.post_id == ^id) defp dynamic_comment_where(:job, id), do: dynamic([c], c.job_id == ^id) defp dynamic_comment_where(:video, id), do: dynamic([c], c.video_id == ^id) + defp dynamic_comment_where(:repo, id), do: dynamic([c], c.repo_id == ^id) defp dynamic_reply_where(:post, comment), do: dynamic([c], c.post_id == ^comment.post_id) defp dynamic_reply_where(:job, comment), do: dynamic([c], c.job_id == ^comment.job_id) defp dynamic_reply_where(:video, comment), do: dynamic([c], c.video_id == ^comment.video_id) + defp dynamic_reply_where(:repo, comment), do: dynamic([c], c.repo_id == ^comment.repo_id) end diff --git a/lib/mastani_server/cms/delegates/comment_reaction.ex b/lib/mastani_server/cms/delegates/comment_reaction.ex index 5b0745ba9..2240b29f0 100644 --- a/lib/mastani_server/cms/delegates/comment_reaction.ex +++ b/lib/mastani_server/cms/delegates/comment_reaction.ex @@ -22,6 +22,7 @@ defmodule MastaniServer.CMS.Delegate.CommentReaction do defp merge_thread_comment_id(:post_comment, comment_id), do: %{post_comment_id: comment_id} defp merge_thread_comment_id(:video_comment, comment_id), do: %{video_comment_id: comment_id} + defp merge_thread_comment_id(:repo_comment, comment_id), do: %{repo_comment_id: comment_id} defp feel_comment(thread, comment_id, user_id, feeling) when valid_feeling(feeling) do diff --git a/lib/mastani_server/cms/repo_comment.ex b/lib/mastani_server/cms/repo_comment.ex new file mode 100644 index 000000000..13ea5dbb9 --- /dev/null +++ b/lib/mastani_server/cms/repo_comment.ex @@ -0,0 +1,43 @@ +defmodule MastaniServer.CMS.RepoComment do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + + alias MastaniServer.CMS.{ + Repo, + RepoCommentDislike, + RepoCommentLike, + RepoCommentReply + } + + @required_fields ~w(body author_id repo_id floor)a + @optional_fields ~w(reply_id)a + + @type t :: %RepoComment{} + schema "repos_comments" do + field(:body, :string) + field(:floor, :integer) + belongs_to(:author, Accounts.User, foreign_key: :author_id) + belongs_to(:repo, Repo, foreign_key: :repo_id) + belongs_to(:reply_to, RepoComment, foreign_key: :reply_id) + + has_many(:replies, {"repos_comments_replies", RepoCommentReply}) + has_many(:likes, {"repos_comments_likes", RepoCommentLike}) + has_many(:dislikes, {"repos_comments_dislikes", RepoCommentDislike}) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%RepoComment{} = repo_comment, attrs) do + repo_comment + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + |> validate_length(:body, min: 1) + |> foreign_key_constraint(:repo_id) + |> foreign_key_constraint(:author_id) + end +end diff --git a/lib/mastani_server/cms/repo_comment_dislike.ex b/lib/mastani_server/cms/repo_comment_dislike.ex new file mode 100644 index 000000000..bcaa2e5a0 --- /dev/null +++ b/lib/mastani_server/cms/repo_comment_dislike.ex @@ -0,0 +1,29 @@ +defmodule MastaniServer.CMS.RepoCommentDislike do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.RepoComment + + @required_fields ~w(repo_comment_id user_id)a + + @type t :: %RepoCommentDislike{} + schema "repos_comments_dislikes" do + belongs_to(:user, Accounts.User, foreign_key: :user_id) + belongs_to(:repo_comment, RepoComment, foreign_key: :repo_comment_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%RepoCommentDislike{} = repo_comment_dislike, attrs) do + repo_comment_dislike + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:repo_comment_id) + |> foreign_key_constraint(:user_id) + |> unique_constraint(:user_id, name: :repos_comments_dislikes_user_id_repo_comment_id_index) + end +end diff --git a/lib/mastani_server/cms/repo_comment_like.ex b/lib/mastani_server/cms/repo_comment_like.ex new file mode 100644 index 000000000..6cf25a43a --- /dev/null +++ b/lib/mastani_server/cms/repo_comment_like.ex @@ -0,0 +1,29 @@ +defmodule MastaniServer.CMS.RepoCommentLike do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.RepoCommentLike + + @required_fields ~w(repo_comment_id user_id)a + + @type t :: %RepoCommentLike{} + schema "repos_comments_likes" do + belongs_to(:user, Accounts.User, foreign_key: :user_id) + belongs_to(:repo_comment, RepoCommentLike, foreign_key: :repo_comment_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%RepoCommentLike{} = repo_comment_like, attrs) do + repo_comment_like + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:repo_comment_id) + |> foreign_key_constraint(:user_id) + |> unique_constraint(:user_id, name: :repos_comments_likes_user_id_repo_comment_id_index) + end +end diff --git a/lib/mastani_server/cms/repo_comment_reply.ex b/lib/mastani_server/cms/repo_comment_reply.ex new file mode 100644 index 000000000..d9ff580a1 --- /dev/null +++ b/lib/mastani_server/cms/repo_comment_reply.ex @@ -0,0 +1,27 @@ +defmodule MastaniServer.CMS.RepoCommentReply do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.CMS.RepoComment + + @required_fields ~w(repo_comment_id reply_id)a + + @type t :: %RepoCommentReply{} + schema "repos_comments_replies" do + belongs_to(:repo_comment, RepoComment, foreign_key: :repo_comment_id) + belongs_to(:reply, RepoComment, foreign_key: :reply_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%RepoCommentReply{} = repo_comment_reply, attrs) do + repo_comment_reply + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:repo_comment_id) + |> foreign_key_constraint(:reply_id) + end +end diff --git a/lib/mastani_server/cms/utils/matcher.ex b/lib/mastani_server/cms/utils/matcher.ex index e50b63d08..45e3c350d 100644 --- a/lib/mastani_server/cms/utils/matcher.ex +++ b/lib/mastani_server/cms/utils/matcher.ex @@ -20,11 +20,14 @@ defmodule MastaniServer.CMS.Utils.Matcher do PostComment, JobComment, VideoComment, + RepoComment, # commtnes reaction PostCommentLike, PostCommentDislike, VideoCommentLike, VideoCommentDislike, + RepoCommentLike, + RepoCommentDislike, # Tag, Community, @@ -114,6 +117,16 @@ defmodule MastaniServer.CMS.Utils.Matcher do def match_action(:repo, :community), do: {:ok, %{target: Repo, reactor: Community, flag: RepoCommunityFlag}} + def match_action(:repo, :comment), + do: {:ok, %{target: Repo, reactor: RepoComment, preload: :author}} + + def match_action(:repo_comment, :like), + do: {:ok, %{target: RepoComment, reactor: RepoCommentLike}} + + def match_action(:repo_comment, :dislike), + do: {:ok, %{target: RepoComment, reactor: RepoCommentDislike}} + + # dynamic where query match def dynamic_where(thread, id) do case thread do :post -> @@ -134,6 +147,12 @@ defmodule MastaniServer.CMS.Utils.Matcher do :video_comment -> {:ok, dynamic([p], p.video_comment_id == ^id)} + :repo -> + {:ok, dynamic([p], p.repo_id == ^id)} + + :repo_comment -> + {:ok, dynamic([p], p.repo_comment_id == ^id)} + _ -> {:error, 'where is not match'} end diff --git a/priv/repo/migrations/20180930030608_create_repo_comment.exs b/priv/repo/migrations/20180930030608_create_repo_comment.exs new file mode 100644 index 000000000..6a959a996 --- /dev/null +++ b/priv/repo/migrations/20180930030608_create_repo_comment.exs @@ -0,0 +1,19 @@ +defmodule MastaniServer.Repo.Migrations.CreateRepoComment do + use Ecto.Migration + + def change do + create table(:repos_comments) do + add(:body, :text) + add(:floor, :integer, default: 0) + add(:author_id, references(:users, on_delete: :delete_all), null: false) + add(:repo_id, references(:cms_repos, on_delete: :delete_all), null: false) + # add(:author_id, references(:users, on_delete: :delete_all), null: false) + add(:reply_id, references(:repos_comments, on_delete: :delete_all)) + + timestamps() + end + + create(index(:repos_comments, [:author_id])) + create(index(:repos_comments, [:repo_id])) + end +end diff --git a/priv/repo/migrations/20180930031135_create_repo_comment_reply.exs b/priv/repo/migrations/20180930031135_create_repo_comment_reply.exs new file mode 100644 index 000000000..cc7d27256 --- /dev/null +++ b/priv/repo/migrations/20180930031135_create_repo_comment_reply.exs @@ -0,0 +1,15 @@ +defmodule MastaniServer.Repo.Migrations.CreateRepoCommentReply do + use Ecto.Migration + + def change do + create table(:repos_comments_replies) do + add(:repo_comment_id, references(:repos_comments, on_delete: :delete_all), null: false) + add(:reply_id, references(:repos_comments, on_delete: :delete_all), null: false) + + timestamps() + end + + create(index(:repos_comments_replies, [:repo_comment_id])) + create(index(:repos_comments_replies, [:reply_id])) + end +end diff --git a/priv/repo/migrations/20180930041418_create_repo_comment_likes.exs b/priv/repo/migrations/20180930041418_create_repo_comment_likes.exs new file mode 100644 index 000000000..2f3336637 --- /dev/null +++ b/priv/repo/migrations/20180930041418_create_repo_comment_likes.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreateRepoCommentLikes do + use Ecto.Migration + + def change do + create table(:repos_comments_likes) do + add(:user_id, references(:users, on_delete: :delete_all), null: false) + add(:repo_comment_id, references(:repos_comments, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:repos_comments_likes, [:user_id, :repo_comment_id])) + end +end diff --git a/priv/repo/migrations/20180930041550_create_repo_comment_dislikes.exs b/priv/repo/migrations/20180930041550_create_repo_comment_dislikes.exs new file mode 100644 index 000000000..e5d81f0b3 --- /dev/null +++ b/priv/repo/migrations/20180930041550_create_repo_comment_dislikes.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreateRepoCommentDislikes do + use Ecto.Migration + + def change do + create table(:repos_comments_dislikes) do + add(:user_id, references(:users, on_delete: :delete_all), null: false) + add(:repo_comment_id, references(:repos_comments, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:repos_comments_dislikes, [:user_id, :repo_comment_id])) + end +end diff --git a/test/mastani_server/cms/repo_comment_test.exs b/test/mastani_server/cms/repo_comment_test.exs new file mode 100644 index 000000000..f94f656d7 --- /dev/null +++ b/test/mastani_server/cms/repo_comment_test.exs @@ -0,0 +1,211 @@ +defmodule MastaniServer.Test.RepoComment do + # currently only test comments for repo type, rename and seprherate later + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.CMS + + alias CMS.{RepoComment, RepoCommentReply} + + setup do + {:ok, repo} = db_insert(:repo) + {:ok, user} = db_insert(:user) + + body = "this is a test comment" + + {:ok, comment} = CMS.create_comment(:repo, repo.id, body, user) + + {:ok, ~m(repo user comment)a} + end + + describe "[comment CURD]" do + test "login user comment to exsiting repo", ~m(repo user)a do + body = "this is a test comment" + + assert {:ok, comment} = CMS.create_comment(:repo, repo.id, body, user) + + assert comment.repo_id == repo.id + assert comment.body == body + assert comment.author_id == user.id + end + + test "created comment should have a increased floor number", ~m(repo user)a do + body = "this is a test comment" + + assert {:ok, comment1} = CMS.create_comment(:repo, repo.id, body, user) + + {:ok, user2} = db_insert(:user) + + assert {:ok, comment2} = CMS.create_comment(:repo, repo.id, body, user2) + + assert comment1.floor == 2 + assert comment2.floor == 3 + end + + test "create comment to non-exsit repo fails", ~m(user)a do + body = "this is a test comment" + + assert {:error, _} = CMS.create_comment(:repo, non_exsit_id(), body, user) + end + + test "can reply a comment, and reply should be in comment replies list", ~m(comment user)a do + reply_body = "this is a reply comment" + + {:ok, reply} = CMS.reply_comment(:repo, comment.id, reply_body, user) + + {:ok, reply_preload} = ORM.find(RepoComment, reply.id, preload: :reply_to) + {:ok, comment_preload} = ORM.find(RepoComment, comment.id, preload: :replies) + + assert reply_preload.reply_to.id == comment.id + assert reply_preload.author_id == user.id + assert reply_preload.body == reply_body + # reply id should be in comments replies list + assert comment_preload.replies |> Enum.any?(&(&1.reply_id == reply.id)) + end + + test "comment can be deleted", ~m(repo user)a do + body = "this is a test comment" + + assert {:ok, comment} = CMS.create_comment(:repo, repo.id, body, user) + + {:ok, deleted} = CMS.delete_comment(:repo, comment.id) + assert deleted.id == comment.id + end + + # TODO it's bug + test "after delete, the coments of id > deleted.id should decrease the floor number", + ~m(repo user)a do + body = "this is a test comment" + # in setup we have a comment + total = 30 + 1 + + comments = + Enum.reduce(1..total, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:repo, repo.id, body, user) + + acc ++ [value] + end) + + [comment_1, comment_2, comment_3, comment_last] = comments |> firstn_and_last(3) + + assert comment_1.floor == 2 + assert comment_2.floor == 3 + assert comment_3.floor == 4 + assert comment_last.floor == total + 1 + + {:ok, _} = CMS.delete_comment(:repo, comment_1.id) + + {:ok, new_comment_2} = ORM.find(RepoComment, comment_2.id) + {:ok, new_comment_3} = ORM.find(RepoComment, comment_3.id) + {:ok, new_comment_last} = ORM.find(RepoComment, comment_last.id) + + assert new_comment_2.floor == 2 + assert new_comment_3.floor == 3 + assert new_comment_last.floor == total + end + + test "comment with replies should be deleted together", ~m(comment user)a do + reply_body = "this is a reply comment" + + {:ok, reply} = CMS.reply_comment(:repo, comment.id, reply_body, user) + + RepoComment |> ORM.find_delete(comment.id) + + {:error, _} = ORM.find(RepoComment, comment.id) + {:error, _} = ORM.find(RepoComment, reply.id) + + {:error, _} = + RepoCommentReply |> ORM.find_by(repo_comment_id: comment.id, reply_id: reply.id) + end + + test "comments pagination should work", ~m(repo user)a do + body = "fake comment" + + Enum.reduce(1..30, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:repo, repo.id, body, user) + + acc ++ [value] + end) + + {:ok, results} = CMS.list_comments(:repo, repo.id, %{page: 1, size: 10}) + + assert results |> is_valid_pagination?(:raw) + end + + test "comment reply can be list one-by-one --> by replied user", ~m(comment)a do + {:ok, user1} = db_insert(:user) + {:ok, user2} = db_insert(:user) + {:ok, user3} = db_insert(:user) + + {:ok, _} = CMS.reply_comment(:repo, comment.id, "reply by user1", user1) + + {:ok, _} = CMS.reply_comment(:repo, comment.id, "reply by user2", user2) + + {:ok, _} = CMS.reply_comment(:repo, comment.id, "reply by user3", user3) + + {:ok, found_reply1} = CMS.list_replies(:repo, comment.id, user1) + assert user1.id == found_reply1 |> List.first() |> Map.get(:author_id) + + {:ok, found_reply2} = CMS.list_replies(:repo, comment.id, user2) + assert user2.id == found_reply2 |> List.first() |> Map.get(:author_id) + + {:ok, found_reply3} = CMS.list_replies(:repo, comment.id, user3) + assert user3.id == found_reply3 |> List.first() |> Map.get(:author_id) + end + end + + describe "[comment Reactions]" do + test "user can like a comment", ~m(comment user)a do + {:ok, liked_comment} = CMS.like_comment(:repo_comment, comment.id, user) + + {:ok, comment_preload} = ORM.find(RepoComment, liked_comment.id, preload: :likes) + + assert comment_preload.likes |> Enum.any?(&(&1.repo_comment_id == comment.id)) + end + + test "user like comment twice fails", ~m(comment user)a do + {:ok, _} = CMS.like_comment(:repo_comment, comment.id, user) + {:error, _error} = CMS.like_comment(:repo_comment, comment.id, user) + end + + test "user can undo a like action", ~m(comment user)a do + {:ok, like} = CMS.like_comment(:repo_comment, comment.id, user) + {:ok, _} = CMS.undo_like_comment(:repo_comment, comment.id, user) + + {:ok, comment_preload} = ORM.find(RepoComment, comment.id, preload: :likes) + assert false == comment_preload.likes |> Enum.any?(&(&1.id == like.id)) + end + + test "user can dislike a comment", ~m(comment user)a do + {:ok, disliked_comment} = CMS.dislike_comment(:repo_comment, comment.id, user) + + {:ok, comment_preload} = ORM.find(RepoComment, disliked_comment.id, preload: :dislikes) + + assert comment_preload.dislikes |> Enum.any?(&(&1.repo_comment_id == comment.id)) + end + + test "user can undo a dislike action", ~m(comment user)a do + {:ok, dislike} = CMS.dislike_comment(:repo_comment, comment.id, user) + {:ok, _} = CMS.undo_dislike_comment(:repo_comment, comment.id, user) + + {:ok, comment_preload} = ORM.find(RepoComment, comment.id, preload: :dislikes) + assert false == comment_preload.dislikes |> Enum.any?(&(&1.id == dislike.id)) + end + + test "user can get paged likes of a repo comment", ~m(comment)a do + {:ok, user1} = db_insert(:user) + {:ok, user2} = db_insert(:user) + {:ok, user3} = db_insert(:user) + + {:ok, _like1} = CMS.like_comment(:repo_comment, comment.id, user1) + {:ok, _like2} = CMS.like_comment(:repo_comment, comment.id, user2) + {:ok, _like3} = CMS.like_comment(:repo_comment, comment.id, user3) + + {:ok, results} = CMS.reaction_users(:repo_comment, :like, comment.id, %{page: 1, size: 10}) + + assert results.entries |> Enum.any?(&(&1.id == user1.id)) + assert results.entries |> Enum.any?(&(&1.id == user2.id)) + assert results.entries |> Enum.any?(&(&1.id == user3.id)) + end + end +end From be7ee5e880374e7a3ab69ba64184cb2c14fd17a5 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 30 Sep 2018 13:15:38 +0800 Subject: [PATCH 027/129] refactor(comments): add a bug tag for spec case the @bug tag means the test case will fails sometimes, but not regular --- Makefile | 2 ++ Makefile.include.mk | 4 ++++ lib/mastani_server/cms/delegates/comment_curd.ex | 16 +++++++--------- test/mastani_server/cms/repo_comment_test.exs | 3 ++- test/mastani_server/cms/video_comment_test.exs | 8 ++++++-- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 11c966202..79449870d 100644 --- a/Makefile +++ b/Makefile @@ -125,6 +125,8 @@ test.watch.wip: mix test.watch --only wip test.watch.wip2: mix test.watch --only wip2 +test.watch.bug: + mix test.watch --only bug test.db_reset: env MIX_ENV=test mix ecto.drop env MIX_ENV=test mix ecto.create diff --git a/Makefile.include.mk b/Makefile.include.mk index b100bb397..a44c3ad61 100644 --- a/Makefile.include.mk +++ b/Makefile.include.mk @@ -130,6 +130,10 @@ define test.help @echo " ....................................................." @echo " test.watch.wip : run @wip test in watch mode" @echo " ....................................................." + @echo " test.watch.wip2 : shortcut for lots of @wip around" + @echo " ....................................................." + @echo " test.watch.bug : sometimes fails for unkown reason" + @echo " ....................................................." @echo " test.db_reset : reset test database" @echo " | needed when add new migration" @echo " ....................................................." diff --git a/lib/mastani_server/cms/delegates/comment_curd.ex b/lib/mastani_server/cms/delegates/comment_curd.ex index e1e9aa737..79dfecdda 100644 --- a/lib/mastani_server/cms/delegates/comment_curd.ex +++ b/lib/mastani_server/cms/delegates/comment_curd.ex @@ -48,16 +48,14 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do ORM.delete(comment) end) |> Multi.run(:update_floor, fn _ -> - ret = - Repo.update_all( - from(p in action.reactor, where: p.id > ^comment.id), - inc: [floor: -1] - ) - |> done() - - case ret do + Repo.update_all( + from(p in action.reactor, where: p.id > ^comment.id), + inc: [floor: -1] + ) + |> done() + |> case do {:ok, _} -> {:ok, comment} - _ -> {:error, ""} + {:error, _} -> {:error, ""} end end) |> Repo.transaction() diff --git a/test/mastani_server/cms/repo_comment_test.exs b/test/mastani_server/cms/repo_comment_test.exs index f94f656d7..022c4f941 100644 --- a/test/mastani_server/cms/repo_comment_test.exs +++ b/test/mastani_server/cms/repo_comment_test.exs @@ -72,7 +72,8 @@ defmodule MastaniServer.Test.RepoComment do assert deleted.id == comment.id end - # TODO it's bug + # TODO may be a bug + @tag :bug test "after delete, the coments of id > deleted.id should decrease the floor number", ~m(repo user)a do body = "this is a test comment" diff --git a/test/mastani_server/cms/video_comment_test.exs b/test/mastani_server/cms/video_comment_test.exs index e627ea825..15f690995 100644 --- a/test/mastani_server/cms/video_comment_test.exs +++ b/test/mastani_server/cms/video_comment_test.exs @@ -72,11 +72,15 @@ defmodule MastaniServer.Test.VideoComment do assert deleted.id == comment.id end - # TODO it's bug + # TODO may be a bug + @tag :bug + # maybe case + # (Postgrex.Error) ERROR 23505 (unique_violation): duplicate key value violates unique constraint "videos_comments_video_id_author_id_floor_index" + # this will crash the server. test "after delete, the coments of id > deleted.id should decrease the floor number", ~m(video user)a do body = "this is a test comment" - # in setup we have a comment + # in setup we have a comment. total = 30 + 1 comments = From 9b5ad22635615be841368e9e086fb666b4fc5726 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 30 Sep 2018 20:38:57 +0800 Subject: [PATCH 028/129] feat(wiki thread): add wiki with test --- lib/mastani_server/cms/cms.ex | 4 ++ lib/mastani_server/cms/community.ex | 6 +- lib/mastani_server/cms/community_wiki.ex | 31 +++++++++ .../cms/delegates/community_sync.ex | 51 ++++++++++++++ lib/mastani_server/cms/github_contributor.ex | 28 ++++++++ .../20180930054149_create_community_wiki.exs | 17 +++++ test/mastani_server/cms/wiki_test.exs | 67 +++++++++++++++++++ test/support/factory.ex | 46 +++++++++---- 8 files changed, 237 insertions(+), 13 deletions(-) create mode 100644 lib/mastani_server/cms/community_wiki.ex create mode 100644 lib/mastani_server/cms/delegates/community_sync.ex create mode 100644 lib/mastani_server/cms/github_contributor.ex create mode 100644 priv/repo/migrations/20180930054149_create_community_wiki.exs create mode 100644 test/mastani_server/cms/wiki_test.exs diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index ee5729cf4..0f8595c48 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -9,6 +9,7 @@ defmodule MastaniServer.CMS do ArticleOperation, ArticleReaction, CommentCURD, + CommunitySync, CommentReaction, CommunityCURD, CommunityOperation, @@ -35,6 +36,9 @@ defmodule MastaniServer.CMS do defdelegate update_tag(attrs), to: CommunityCURD defdelegate get_tags(community, thread), to: CommunityCURD defdelegate get_tags(filter), to: CommunityCURD + # >> wiki & cheatsheet (sync with github) + defdelegate sync_content(community, thread, attrs), to: CommunitySync + defdelegate add_contributor(wiki, attrs), to: CommunitySync # CommunityOperation # >> category diff --git a/lib/mastani_server/cms/community.ex b/lib/mastani_server/cms/community.ex index 2063636d0..4a4ac8ceb 100644 --- a/lib/mastani_server/cms/community.ex +++ b/lib/mastani_server/cms/community.ex @@ -12,7 +12,8 @@ defmodule MastaniServer.CMS.Community do Job, CommunityThread, CommunitySubscriber, - CommunityEditor + CommunityEditor, + CommunityWiki } alias MastaniServer.Accounts @@ -36,6 +37,9 @@ defmodule MastaniServer.CMS.Community do has_many(:subscribers, {"communities_subscribers", CommunitySubscriber}) has_many(:editors, {"communities_editors", CommunityEditor}) + has_one(:wiki, CommunityWiki) + # has_one(:cheatsheet, CommunityCheatsheet) + many_to_many( :categories, Category, diff --git a/lib/mastani_server/cms/community_wiki.ex b/lib/mastani_server/cms/community_wiki.ex new file mode 100644 index 000000000..7def8a2e3 --- /dev/null +++ b/lib/mastani_server/cms/community_wiki.ex @@ -0,0 +1,31 @@ +defmodule MastaniServer.CMS.CommunityWiki do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + + alias MastaniServer.CMS.{Community, GithubContributor} + + @required_fields ~w(community_id last_sync)a + @optional_fields ~w(readme)a + + @type t :: %CommunityWiki{} + schema "community_wikis" do + belongs_to(:community, Community) + + field(:readme, :string) + embeds_many(:contributors, GithubContributor, on_replace: :delete) + field(:last_sync, :utc_datetime) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%CommunityWiki{} = community_wiki, attrs) do + community_wiki + |> cast(attrs, @required_fields ++ @optional_fields) + |> cast_embed(:contributors, with: &GithubContributor.changeset/2) + |> validate_required(@required_fields) + end +end diff --git a/lib/mastani_server/cms/delegates/community_sync.ex b/lib/mastani_server/cms/delegates/community_sync.ex new file mode 100644 index 000000000..e9dd20fd5 --- /dev/null +++ b/lib/mastani_server/cms/delegates/community_sync.ex @@ -0,0 +1,51 @@ +defmodule MastaniServer.CMS.Delegate.CommunitySync do + @moduledoc """ + community curd + """ + import Ecto.Query, warn: false + import Helper.ErrorCode + import ShortMaps + + alias Helper.ORM + + alias MastaniServer.CMS.{ + Community, + CommunityWiki + } + + @doc """ + return paged community subscribers + """ + def sync_content(%Community{id: id}, :wiki, attrs) do + with {:ok, community} <- ORM.find(Community, id) do + attrs = Map.merge(attrs, %{community_id: community.id}) + + CommunityWiki |> ORM.upsert_by([community_id: community.id], attrs) + end + end + + @doc """ + add contributor to exsit wiki contributors list + """ + def add_contributor(%CommunityWiki{id: id}, contributor_attrs) do + do_add_contributor(CommunityWiki, id, contributor_attrs) + end + + defp do_add_contributor(queryable, id, contributor_attrs) do + with {:ok, content} <- ORM.find(queryable, id) do + cur_contributors = + Enum.reduce(content.contributors, [], fn user, acc -> + acc ++ [Map.from_struct(user)] + end) + + case cur_contributors |> Enum.any?(&(&1.github_id == contributor_attrs.github_id)) do + true -> + {:error, [message: "already added", code: ecode(:already_exsit)]} + + false -> + new_contributors = %{contributors: cur_contributors ++ [contributor_attrs]} + content |> ORM.update(new_contributors) + end + end + end +end diff --git a/lib/mastani_server/cms/github_contributor.ex b/lib/mastani_server/cms/github_contributor.ex new file mode 100644 index 000000000..0fffff054 --- /dev/null +++ b/lib/mastani_server/cms/github_contributor.ex @@ -0,0 +1,28 @@ +defmodule MastaniServer.CMS.GithubContributor do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + + @required_fields ~w(github_id avatar nickname html_url)a + @optional_fields ~w(bio company location)a + + @type t :: %GithubContributor{} + embedded_schema do + field(:github_id, :string) + field(:avatar, :string) + field(:nickname, :string) + field(:bio, :string) + field(:company, :string) + field(:location, :string) + field(:html_url, :string) + end + + @doc false + def changeset(%GithubContributor{} = github_contributor, attrs) do + github_contributor + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + end +end diff --git a/priv/repo/migrations/20180930054149_create_community_wiki.exs b/priv/repo/migrations/20180930054149_create_community_wiki.exs new file mode 100644 index 000000000..afaeb2e38 --- /dev/null +++ b/priv/repo/migrations/20180930054149_create_community_wiki.exs @@ -0,0 +1,17 @@ +defmodule MastaniServer.Repo.Migrations.CreateCommunityWiki do + use Ecto.Migration + + def change do + create table(:community_wikis) do + add(:community_id, references(:communities, on_delete: :delete_all), null: false) + add(:readme, :text) + # this should be a embed schema + add(:contributors, :map) + add(:last_sync, :utc_datetime) + + timestamps() + end + + create(index(:community_wikis, [:community_id])) + end +end diff --git a/test/mastani_server/cms/wiki_test.exs b/test/mastani_server/cms/wiki_test.exs new file mode 100644 index 000000000..29791457a --- /dev/null +++ b/test/mastani_server/cms/wiki_test.exs @@ -0,0 +1,67 @@ +defmodule MastaniServer.Test.Wiki do + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.CMS + + alias CMS.{CommunityWiki} + + setup do + {:ok, user} = db_insert(:user) + # {:ok, post} = db_insert(:post) + {:ok, community} = db_insert(:community) + + wiki_attrs = mock_attrs(:wiki, %{community_id: community.id}) + + {:ok, ~m(user community wiki_attrs)a} + end + + describe "[cms wiki sync]" do + @tag :wip + test "can create create/sync a wiki to a community", ~m(community wiki_attrs)a do + {:ok, wiki} = CMS.sync_content(community, :wiki, wiki_attrs) + + assert wiki.community_id == community.id + assert wiki.last_sync == wiki_attrs.last_sync + end + + @tag :wip + test "can update a exsit wiki", ~m(community wiki_attrs)a do + {:ok, wiki} = CMS.sync_content(community, :wiki, wiki_attrs) + + new_wiki_attrs = mock_attrs(:wiki, %{community_id: community.id, readme: "new readme"}) + {:ok, _} = CMS.sync_content(community, :wiki, new_wiki_attrs) + {:ok, new_wiki} = CommunityWiki |> ORM.find(wiki.id) + + assert new_wiki.readme == "new readme" + end + + @tag :wip + test "can add contributor to wiki", ~m(community wiki_attrs)a do + {:ok, wiki} = CMS.sync_content(community, :wiki, wiki_attrs) + cur_contributors = wiki.contributors + + contributor_attrs = mock_attrs(:github_contributor) + {:ok, wiki} = CMS.add_contributor(wiki, contributor_attrs) + update_contributors = wiki.contributors + + assert length(update_contributors) == 1 + length(cur_contributors) + end + + @tag :wip + test "add some contributor fails", ~m(community wiki_attrs)a do + {:ok, wiki} = CMS.sync_content(community, :wiki, wiki_attrs) + cur_contributors = wiki.contributors + + contributor_attrs = mock_attrs(:github_contributor) + {:ok, wiki} = CMS.add_contributor(wiki, contributor_attrs) + update_contributors = wiki.contributors + + assert length(update_contributors) == 1 + length(cur_contributors) + + # add some again + {:error, error} = CMS.add_contributor(wiki, contributor_attrs) + assert error |> Keyword.get(:code) == ecode(:already_exsit) + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index 1924fa8fe..bebd6adb5 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -94,18 +94,8 @@ defmodule MastaniServer.Support.Factory do color: "tomato" }, contributors: [ - %{ - avatar: Faker.Avatar.image_url(), - html_url: Faker.Avatar.image_url(), - htmlUrl: Faker.Avatar.image_url(), - nickname: "mydearxym" - }, - %{ - avatar: Faker.Avatar.image_url(), - html_url: Faker.Avatar.image_url(), - htmlUrl: Faker.Avatar.image_url(), - nickname: "mydearxym2" - } + mock_meta(:github_contributor), + mock_meta(:github_contributor) ], author: mock(:author), views: Enum.random(0..2000), @@ -116,6 +106,33 @@ defmodule MastaniServer.Support.Factory do } end + defp mock_meta(:wiki) do + %{ + readme: Faker.Lorem.sentence(%Range{first: 15, last: 60}), + last_sync: Timex.today() |> Timex.to_datetime(), + contributors: [ + mock_meta(:github_contributor), + mock_meta(:github_contributor), + mock_meta(:github_contributor) + ] + } + end + + defp mock_meta(:github_contributor) do + unique_num = System.unique_integer([:positive, :monotonic]) + + %{ + github_id: "#{unique_num}-#{Faker.Lorem.sentence(%Range{first: 5, last: 10})}", + avatar: Faker.Avatar.image_url(), + html_url: Faker.Avatar.image_url(), + htmlUrl: Faker.Avatar.image_url(), + nickname: "mydearxym2", + bio: Faker.Lorem.sentence(%Range{first: 15, last: 60}), + location: "location #{unique_num}", + company: Faker.Company.name() + } + end + defp mock_meta(:job) do body = Faker.Lorem.sentence(%Range{first: 80, last: 120}) unique_num = System.unique_integer([:positive, :monotonic]) @@ -259,6 +276,11 @@ defmodule MastaniServer.Support.Factory do def mock_attrs(:thread, attrs), do: mock_meta(:thread) |> Map.merge(attrs) def mock_attrs(:mention, attrs), do: mock_meta(:mention) |> Map.merge(attrs) + def mock_attrs(:wiki, attrs), do: mock_meta(:wiki) |> Map.merge(attrs) + + def mock_attrs(:github_contributor, attrs), + do: mock_meta(:github_contributor) |> Map.merge(attrs) + def mock_attrs(:communities_threads, attrs), do: mock_meta(:communities_threads) |> Map.merge(attrs) From bec08ecffd60be3ef1b2e007dd698c28f178d320 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 30 Sep 2018 21:27:40 +0800 Subject: [PATCH 029/129] feat(wiki thread): add basic wiki query --- lib/mastani_server/cms/cms.ex | 1 + lib/mastani_server/cms/community_wiki.ex | 2 + .../cms/delegates/community_sync.ex | 10 ++++ .../resolvers/cms_resolver.ex | 2 + .../schema/cms/cms_queries.ex | 6 ++ .../schema/cms/cms_types.ex | 20 +++++++ ...0930131449_add_views_to_community_wiki.exs | 9 +++ .../query/cms/wiki_test.exs | 57 +++++++++++++++++++ test/support/factory.ex | 2 + 9 files changed, 109 insertions(+) create mode 100644 priv/repo/migrations/20180930131449_add_views_to_community_wiki.exs create mode 100644 test/mastani_server_web/query/cms/wiki_test.exs diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index 0f8595c48..39e93a143 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -37,6 +37,7 @@ defmodule MastaniServer.CMS do defdelegate get_tags(community, thread), to: CommunityCURD defdelegate get_tags(filter), to: CommunityCURD # >> wiki & cheatsheet (sync with github) + defdelegate get_wiki(community), to: CommunitySync defdelegate sync_content(community, thread, attrs), to: CommunitySync defdelegate add_contributor(wiki, attrs), to: CommunitySync diff --git a/lib/mastani_server/cms/community_wiki.ex b/lib/mastani_server/cms/community_wiki.ex index 7def8a2e3..ac85f6da6 100644 --- a/lib/mastani_server/cms/community_wiki.ex +++ b/lib/mastani_server/cms/community_wiki.ex @@ -18,6 +18,8 @@ defmodule MastaniServer.CMS.CommunityWiki do embeds_many(:contributors, GithubContributor, on_replace: :delete) field(:last_sync, :utc_datetime) + field(:views, :integer, default: 0) + timestamps(type: :utc_datetime) end diff --git a/lib/mastani_server/cms/delegates/community_sync.ex b/lib/mastani_server/cms/delegates/community_sync.ex index e9dd20fd5..347461d01 100644 --- a/lib/mastani_server/cms/delegates/community_sync.ex +++ b/lib/mastani_server/cms/delegates/community_sync.ex @@ -13,6 +13,16 @@ defmodule MastaniServer.CMS.Delegate.CommunitySync do CommunityWiki } + @doc """ + get wiki + """ + def get_wiki(%Community{raw: raw}) do + with {:ok, community} <- ORM.find_by(Community, raw: raw), + {:ok, wiki} <- ORM.find_by(CommunityWiki, community_id: community.id) do + CommunityWiki |> ORM.read(wiki.id, inc: :views) + end + end + @doc """ return paged community subscribers """ diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 64c2cdc84..9f8edeb72 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -37,6 +37,8 @@ defmodule MastaniServerWeb.Resolvers.CMS do def repo(_root, %{id: id}, _info), do: Repo |> ORM.read(id, inc: :views) def job(_root, %{id: id}, _info), do: Job |> ORM.read(id, inc: :views) + def wiki(_root, ~m(community)a, _info), do: CMS.get_wiki(%Community{raw: community}) + def paged_posts(_root, ~m(filter)a, _info), do: Post |> CMS.paged_contents(filter) def paged_videos(_root, ~m(filter)a, _info), do: Video |> CMS.paged_contents(filter) def paged_repos(_root, ~m(filter)a, _info), do: Repo |> CMS.paged_contents(filter) diff --git a/lib/mastani_server_web/schema/cms/cms_queries.ex b/lib/mastani_server_web/schema/cms/cms_queries.ex index 2e0c72979..d31f0f96d 100644 --- a/lib/mastani_server_web/schema/cms/cms_queries.ex +++ b/lib/mastani_server_web/schema/cms/cms_queries.ex @@ -106,6 +106,12 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do resolve(&R.CMS.paged_repos/3) end + @desc "get wiki by community raw name" + field :wiki, non_null(:wiki) do + arg(:community, :string) + resolve(&R.CMS.wiki/3) + end + @desc "get job by id" field :job, non_null(:job) do arg(:id, non_null(:id)) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index f85b07f44..c1a6b44af 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -323,6 +323,26 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:updated_at, :datetime) end + object :github_contributor do + field(:avatar, :string) + field(:html_url, :string) + field(:nickname, :string) + field(:location, :string) + field(:company, :string) + end + + object :wiki do + field(:id, :id) + field(:readme, :string) + field(:contributors, list_of(:github_contributor)) + + field(:last_sync, :datetime) + field(:views, :integer) + + field(:inserted_at, :datetime) + field(:updated_at, :datetime) + end + object :thread do field(:id, :id) field(:title, :string) diff --git a/priv/repo/migrations/20180930131449_add_views_to_community_wiki.exs b/priv/repo/migrations/20180930131449_add_views_to_community_wiki.exs new file mode 100644 index 000000000..698c42929 --- /dev/null +++ b/priv/repo/migrations/20180930131449_add_views_to_community_wiki.exs @@ -0,0 +1,9 @@ +defmodule MastaniServer.Repo.Migrations.AddViewsToCommunityWiki do + use Ecto.Migration + + def change do + alter table(:community_wikis) do + add(:views, :integer, default: 0) + end + end +end diff --git a/test/mastani_server_web/query/cms/wiki_test.exs b/test/mastani_server_web/query/cms/wiki_test.exs new file mode 100644 index 000000000..a22317983 --- /dev/null +++ b/test/mastani_server_web/query/cms/wiki_test.exs @@ -0,0 +1,57 @@ +defmodule MastaniServer.Test.Query.Wiki do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, community} = db_insert(:community) + + wiki_attrs = mock_attrs(:wiki, %{community_id: community.id}) + {:ok, wiki} = CMS.sync_content(community, :wiki, wiki_attrs) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user) + + {:ok, ~m(user_conn guest_conn community wiki)a} + end + + @query """ + query($community: String!) { + wiki(community: $community) { + id + readme + contributors { + avatar + nickname + } + } + } + """ + @tag :wip + test "basic graphql query on wiki", ~m(guest_conn community wiki)a do + variables = %{community: community.raw} + results = guest_conn |> query_result(@query, variables, "wiki") + + assert results["id"] == to_string(wiki.id) + assert is_valid_kv?(results, "readme", :string) + assert results["contributors"] |> length !== 0 + end + + @query """ + query($community: String!) { + wiki(community: $community) { + views + } + } + """ + @tag :wip + test "views should +1 after query the wiki", ~m(guest_conn community)a do + variables = %{community: community.raw} + views_1 = guest_conn |> query_result(@query, variables, "wiki") |> Map.get("views") + + variables = %{community: community.raw} + views_2 = guest_conn |> query_result(@query, variables, "wiki") |> Map.get("views") + + assert views_2 == views_1 + 1 + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index bebd6adb5..cf1083846 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -108,6 +108,7 @@ defmodule MastaniServer.Support.Factory do defp mock_meta(:wiki) do %{ + community: mock(:community), readme: Faker.Lorem.sentence(%Range{first: 15, last: 60}), last_sync: Timex.today() |> Timex.to_datetime(), contributors: [ @@ -301,6 +302,7 @@ defmodule MastaniServer.Support.Factory do defp mock(:video), do: CMS.Video |> struct(mock_meta(:video)) defp mock(:repo), do: CMS.Repo |> struct(mock_meta(:repo)) defp mock(:job), do: CMS.Job |> struct(mock_meta(:job)) + defp mock(:wiki), do: CMS.CommunityWiki |> struct(mock_meta(:wiki)) defp mock(:comment), do: CMS.Comment |> struct(mock_meta(:comment)) defp mock(:mention), do: Delivery.Mention |> struct(mock_meta(:mention)) defp mock(:author), do: CMS.Author |> struct(mock_meta(:author)) From 8fc9f671452abac62400e82d4a3442a0ad2ea4f1 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 30 Sep 2018 23:06:29 +0800 Subject: [PATCH 030/129] feat(wiki thread): add sync content and add contributor --- lib/mastani_server/cms/cms.ex | 4 +- .../cms/delegates/community_sync.ex | 2 +- .../resolvers/cms_resolver.ex | 11 +++ lib/mastani_server_web/schema/cms/cms_misc.ex | 13 +++ .../schema/cms/mutations/community.ex | 18 ++++ test/mastani_server/cms/wiki_test.exs | 10 +-- .../mutation/cms/wiki_test.exs | 87 +++++++++++++++++++ .../query/cms/wiki_test.exs | 2 +- test/support/factory.ex | 2 + 9 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 test/mastani_server_web/mutation/cms/wiki_test.exs diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index 39e93a143..2367be8d9 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -38,8 +38,8 @@ defmodule MastaniServer.CMS do defdelegate get_tags(filter), to: CommunityCURD # >> wiki & cheatsheet (sync with github) defdelegate get_wiki(community), to: CommunitySync - defdelegate sync_content(community, thread, attrs), to: CommunitySync - defdelegate add_contributor(wiki, attrs), to: CommunitySync + defdelegate sync_github_content(community, thread, attrs), to: CommunitySync + defdelegate add_contributor(content, attrs), to: CommunitySync # CommunityOperation # >> category diff --git a/lib/mastani_server/cms/delegates/community_sync.ex b/lib/mastani_server/cms/delegates/community_sync.ex index 347461d01..05917b881 100644 --- a/lib/mastani_server/cms/delegates/community_sync.ex +++ b/lib/mastani_server/cms/delegates/community_sync.ex @@ -26,7 +26,7 @@ defmodule MastaniServer.CMS.Delegate.CommunitySync do @doc """ return paged community subscribers """ - def sync_content(%Community{id: id}, :wiki, attrs) do + def sync_github_content(%Community{id: id}, :wiki, attrs) do with {:ok, community} <- ORM.find(Community, id) do attrs = Map.merge(attrs, %{community_id: community.id}) diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 9f8edeb72..10bb3b986 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -248,4 +248,15 @@ defmodule MastaniServerWeb.Resolvers.CMS do def stamp_passport(_root, ~m(user_id rules)a, %{context: %{cur_user: _user}}) do CMS.stamp_passport(rules, %User{id: user_id}) end + + # ####################### + # sync github content .. + # ####################### + def sync_wiki(_root, ~m(community_id readme last_sync)a, %{context: %{cur_user: _user}}) do + CMS.sync_github_content(%Community{id: community_id}, :wiki, ~m(readme last_sync)a) + end + + def add_wiki_contributor(_root, ~m(id contributor)a, %{context: %{cur_user: _user}}) do + CMS.add_contributor(%CMS.CommunityWiki{id: id}, contributor) + end end diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index 940214a23..f0fcecb80 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -164,6 +164,19 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do field(:nickname, :string) end + @doc """ + cms github repo contribotor, detail version + """ + input_object :github_contributor_input do + field(:github_id, non_null(:string)) + field(:avatar, non_null(:string)) + field(:html_url, non_null(:string)) + field(:nickname, non_null(:string)) + field(:bio, :string) + field(:location, :string) + field(:company, :string) + end + @doc """ cms github repo lang """ diff --git a/lib/mastani_server_web/schema/cms/mutations/community.ex b/lib/mastani_server_web/schema/cms/mutations/community.ex index 5eee46611..a60731c42 100644 --- a/lib/mastani_server_web/schema/cms/mutations/community.ex +++ b/lib/mastani_server_web/schema/cms/mutations/community.ex @@ -170,5 +170,23 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Community do resolve(&R.CMS.delete_tag/3) end + + @desc "sync github docs like wiki / cheatsheets " + field :sync_wiki, :wiki do + arg(:community_id, non_null(:id)) + arg(:readme, non_null(:string)) + arg(:last_sync, non_null(:datetime)) + + middleware(M.Authorize, :login) + resolve(&R.CMS.sync_wiki/3) + end + + field :add_wiki_contributor, :wiki do + arg(:id, non_null(:id)) + arg(:contributor, non_null(:github_contributor_input)) + + middleware(M.Authorize, :login) + resolve(&R.CMS.add_wiki_contributor/3) + end end end diff --git a/test/mastani_server/cms/wiki_test.exs b/test/mastani_server/cms/wiki_test.exs index 29791457a..c0a0b99e8 100644 --- a/test/mastani_server/cms/wiki_test.exs +++ b/test/mastani_server/cms/wiki_test.exs @@ -19,7 +19,7 @@ defmodule MastaniServer.Test.Wiki do describe "[cms wiki sync]" do @tag :wip test "can create create/sync a wiki to a community", ~m(community wiki_attrs)a do - {:ok, wiki} = CMS.sync_content(community, :wiki, wiki_attrs) + {:ok, wiki} = CMS.sync_github_content(community, :wiki, wiki_attrs) assert wiki.community_id == community.id assert wiki.last_sync == wiki_attrs.last_sync @@ -27,10 +27,10 @@ defmodule MastaniServer.Test.Wiki do @tag :wip test "can update a exsit wiki", ~m(community wiki_attrs)a do - {:ok, wiki} = CMS.sync_content(community, :wiki, wiki_attrs) + {:ok, wiki} = CMS.sync_github_content(community, :wiki, wiki_attrs) new_wiki_attrs = mock_attrs(:wiki, %{community_id: community.id, readme: "new readme"}) - {:ok, _} = CMS.sync_content(community, :wiki, new_wiki_attrs) + {:ok, _} = CMS.sync_github_content(community, :wiki, new_wiki_attrs) {:ok, new_wiki} = CommunityWiki |> ORM.find(wiki.id) assert new_wiki.readme == "new readme" @@ -38,7 +38,7 @@ defmodule MastaniServer.Test.Wiki do @tag :wip test "can add contributor to wiki", ~m(community wiki_attrs)a do - {:ok, wiki} = CMS.sync_content(community, :wiki, wiki_attrs) + {:ok, wiki} = CMS.sync_github_content(community, :wiki, wiki_attrs) cur_contributors = wiki.contributors contributor_attrs = mock_attrs(:github_contributor) @@ -50,7 +50,7 @@ defmodule MastaniServer.Test.Wiki do @tag :wip test "add some contributor fails", ~m(community wiki_attrs)a do - {:ok, wiki} = CMS.sync_content(community, :wiki, wiki_attrs) + {:ok, wiki} = CMS.sync_github_content(community, :wiki, wiki_attrs) cur_contributors = wiki.contributors contributor_attrs = mock_attrs(:github_contributor) diff --git a/test/mastani_server_web/mutation/cms/wiki_test.exs b/test/mastani_server_web/mutation/cms/wiki_test.exs new file mode 100644 index 000000000..4f019c700 --- /dev/null +++ b/test/mastani_server_web/mutation/cms/wiki_test.exs @@ -0,0 +1,87 @@ +defmodule MastaniServer.Test.Mutation.CMS.Wiki do + use MastaniServer.TestTools + + alias MastaniServer.CMS + alias MastaniServer.Statistics + + alias CMS.{Community} + + alias Helper.ORM + + setup do + {:ok, user} = db_insert(:user) + {:ok, community} = db_insert(:community) + + wiki_attrs = mock_attrs(:wiki, %{community_id: community.id}) + {:ok, wiki} = CMS.sync_github_content(community, :wiki, wiki_attrs) + + user_conn = simu_conn(:user) + guest_conn = simu_conn(:guest) + + {:ok, ~m(user_conn guest_conn community user wiki)a} + end + + @sync_wiki_query """ + mutation($communityId: ID!, $readme: String!, $lastSync: String!){ + syncWiki(communityId: $communityId, readme: $readme, lastSync: $lastSync) { + id + readme + } + } + """ + @tag :wip + test "login user can sync wiki", ~m(community)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + wiki_attrs = mock_attrs(:wiki) + + variables = wiki_attrs |> Map.merge(%{communityId: community.id}) + created = user_conn |> mutation_result(@sync_wiki_query, variables, "syncWiki") + + {:ok, wiki} = ORM.find(CMS.CommunityWiki, created["id"]) + + assert created["id"] == to_string(wiki.id) + assert created["readme"] == to_string(wiki.readme) + end + + @add_wiki_contribotor_query """ + mutation($id: ID!, $contributor: GithubContributorInput!){ + addWikiContributor(id: $id, contributor: $contributor) { + id + readme + contributors { + nickname + } + } + } + """ + @tag :wip + test "login user can add contributor to an exsit wiki", ~m(user wiki)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + contributor_attrs = mock_attrs(:github_contributor) + variables = %{id: wiki.id, contributor: contributor_attrs} + + created = + user_conn |> mutation_result(@add_wiki_contribotor_query, variables, "addWikiContributor") + + assert created["contributors"] |> length == 4 + end + + @tag :wip + test "add some contributor fails", ~m(user wiki)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + contributor_attrs = mock_attrs(:github_contributor) + variables = %{id: wiki.id, contributor: contributor_attrs} + + created = + user_conn |> mutation_result(@add_wiki_contribotor_query, variables, "addWikiContributor") + + assert user_conn + |> mutation_get_error?(@add_wiki_contribotor_query, variables, ecode(:already_exsit)) + end +end diff --git a/test/mastani_server_web/query/cms/wiki_test.exs b/test/mastani_server_web/query/cms/wiki_test.exs index a22317983..7f6675d7c 100644 --- a/test/mastani_server_web/query/cms/wiki_test.exs +++ b/test/mastani_server_web/query/cms/wiki_test.exs @@ -7,7 +7,7 @@ defmodule MastaniServer.Test.Query.Wiki do {:ok, community} = db_insert(:community) wiki_attrs = mock_attrs(:wiki, %{community_id: community.id}) - {:ok, wiki} = CMS.sync_content(community, :wiki, wiki_attrs) + {:ok, wiki} = CMS.sync_github_content(community, :wiki, wiki_attrs) guest_conn = simu_conn(:guest) user_conn = simu_conn(:user) diff --git a/test/support/factory.ex b/test/support/factory.ex index cf1083846..9fa46aa2c 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -111,6 +111,7 @@ defmodule MastaniServer.Support.Factory do community: mock(:community), readme: Faker.Lorem.sentence(%Range{first: 15, last: 60}), last_sync: Timex.today() |> Timex.to_datetime(), + lastSync: "2017-11-01T12:00:00Z", contributors: [ mock_meta(:github_contributor), mock_meta(:github_contributor), @@ -124,6 +125,7 @@ defmodule MastaniServer.Support.Factory do %{ github_id: "#{unique_num}-#{Faker.Lorem.sentence(%Range{first: 5, last: 10})}", + githubId: "#{unique_num}-#{Faker.Lorem.sentence(%Range{first: 5, last: 10})}", avatar: Faker.Avatar.image_url(), html_url: Faker.Avatar.image_url(), htmlUrl: Faker.Avatar.image_url(), From 37a2eca69272c5153c5e9e4885463c7ea5168b24 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 30 Sep 2018 23:08:08 +0800 Subject: [PATCH 031/129] chore(wiki thread): clean up wiki --- test/mastani_server/cms/wiki_test.exs | 4 ---- test/mastani_server_web/mutation/cms/wiki_test.exs | 3 --- test/mastani_server_web/query/cms/wiki_test.exs | 2 -- 3 files changed, 9 deletions(-) diff --git a/test/mastani_server/cms/wiki_test.exs b/test/mastani_server/cms/wiki_test.exs index c0a0b99e8..43bf10871 100644 --- a/test/mastani_server/cms/wiki_test.exs +++ b/test/mastani_server/cms/wiki_test.exs @@ -17,7 +17,6 @@ defmodule MastaniServer.Test.Wiki do end describe "[cms wiki sync]" do - @tag :wip test "can create create/sync a wiki to a community", ~m(community wiki_attrs)a do {:ok, wiki} = CMS.sync_github_content(community, :wiki, wiki_attrs) @@ -25,7 +24,6 @@ defmodule MastaniServer.Test.Wiki do assert wiki.last_sync == wiki_attrs.last_sync end - @tag :wip test "can update a exsit wiki", ~m(community wiki_attrs)a do {:ok, wiki} = CMS.sync_github_content(community, :wiki, wiki_attrs) @@ -36,7 +34,6 @@ defmodule MastaniServer.Test.Wiki do assert new_wiki.readme == "new readme" end - @tag :wip test "can add contributor to wiki", ~m(community wiki_attrs)a do {:ok, wiki} = CMS.sync_github_content(community, :wiki, wiki_attrs) cur_contributors = wiki.contributors @@ -48,7 +45,6 @@ defmodule MastaniServer.Test.Wiki do assert length(update_contributors) == 1 + length(cur_contributors) end - @tag :wip test "add some contributor fails", ~m(community wiki_attrs)a do {:ok, wiki} = CMS.sync_github_content(community, :wiki, wiki_attrs) cur_contributors = wiki.contributors diff --git a/test/mastani_server_web/mutation/cms/wiki_test.exs b/test/mastani_server_web/mutation/cms/wiki_test.exs index 4f019c700..453343c7d 100644 --- a/test/mastani_server_web/mutation/cms/wiki_test.exs +++ b/test/mastani_server_web/mutation/cms/wiki_test.exs @@ -29,7 +29,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Wiki do } } """ - @tag :wip test "login user can sync wiki", ~m(community)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -56,7 +55,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Wiki do } } """ - @tag :wip test "login user can add contributor to an exsit wiki", ~m(user wiki)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -70,7 +68,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Wiki do assert created["contributors"] |> length == 4 end - @tag :wip test "add some contributor fails", ~m(user wiki)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) diff --git a/test/mastani_server_web/query/cms/wiki_test.exs b/test/mastani_server_web/query/cms/wiki_test.exs index 7f6675d7c..df49c41b1 100644 --- a/test/mastani_server_web/query/cms/wiki_test.exs +++ b/test/mastani_server_web/query/cms/wiki_test.exs @@ -27,7 +27,6 @@ defmodule MastaniServer.Test.Query.Wiki do } } """ - @tag :wip test "basic graphql query on wiki", ~m(guest_conn community wiki)a do variables = %{community: community.raw} results = guest_conn |> query_result(@query, variables, "wiki") @@ -44,7 +43,6 @@ defmodule MastaniServer.Test.Query.Wiki do } } """ - @tag :wip test "views should +1 after query the wiki", ~m(guest_conn community)a do variables = %{community: community.raw} views_1 = guest_conn |> query_result(@query, variables, "wiki") |> Map.get("views") From b303c106e921d2e0da9f9f0c331da6a47df4cee9 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 30 Sep 2018 23:38:10 +0800 Subject: [PATCH 032/129] feat(cheatsheet): add feature just like wiki --- lib/mastani_server/cms/cms.ex | 1 + lib/mastani_server/cms/community.ex | 5 +- .../cms/community_cheatsheet.ex | 33 +++++++ .../cms/delegates/community_sync.ex | 30 +++++- .../resolvers/cms_resolver.ex | 8 ++ lib/mastani_server_web/schema/cms/cms_misc.ex | 1 + .../schema/cms/cms_types.ex | 12 +++ .../schema/cms/mutations/community.ex | 22 ++++- ...30151113_create__community_cheatsheets.exs | 18 ++++ test/mastani_server/cms/cheatsheet_test.exs | 69 ++++++++++++++ .../mutation/cms/cheatsheet_test.exs | 91 +++++++++++++++++++ .../mutation/cms/repo_test.exs | 1 + test/support/factory.ex | 19 +++- 13 files changed, 303 insertions(+), 7 deletions(-) create mode 100644 lib/mastani_server/cms/community_cheatsheet.ex create mode 100644 priv/repo/migrations/20180930151113_create__community_cheatsheets.exs create mode 100644 test/mastani_server/cms/cheatsheet_test.exs create mode 100644 test/mastani_server_web/mutation/cms/cheatsheet_test.exs diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index 2367be8d9..23d229220 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -38,6 +38,7 @@ defmodule MastaniServer.CMS do defdelegate get_tags(filter), to: CommunityCURD # >> wiki & cheatsheet (sync with github) defdelegate get_wiki(community), to: CommunitySync + defdelegate get_cheatsheet(community), to: CommunitySync defdelegate sync_github_content(community, thread, attrs), to: CommunitySync defdelegate add_contributor(content, attrs), to: CommunitySync diff --git a/lib/mastani_server/cms/community.ex b/lib/mastani_server/cms/community.ex index 4a4ac8ceb..f5b88d0fc 100644 --- a/lib/mastani_server/cms/community.ex +++ b/lib/mastani_server/cms/community.ex @@ -13,7 +13,8 @@ defmodule MastaniServer.CMS.Community do CommunityThread, CommunitySubscriber, CommunityEditor, - CommunityWiki + CommunityWiki, + CommunityCheatsheet } alias MastaniServer.Accounts @@ -38,7 +39,7 @@ defmodule MastaniServer.CMS.Community do has_many(:editors, {"communities_editors", CommunityEditor}) has_one(:wiki, CommunityWiki) - # has_one(:cheatsheet, CommunityCheatsheet) + has_one(:cheatsheet, CommunityCheatsheet) many_to_many( :categories, diff --git a/lib/mastani_server/cms/community_cheatsheet.ex b/lib/mastani_server/cms/community_cheatsheet.ex new file mode 100644 index 000000000..6dbf3d923 --- /dev/null +++ b/lib/mastani_server/cms/community_cheatsheet.ex @@ -0,0 +1,33 @@ +defmodule MastaniServer.CMS.CommunityCheatsheet do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + + alias MastaniServer.CMS.{Community, GithubContributor} + + @required_fields ~w(community_id last_sync)a + @optional_fields ~w(readme)a + + @type t :: %CommunityCheatsheet{} + schema "community_cheatsheets" do + belongs_to(:community, Community) + + field(:readme, :string) + embeds_many(:contributors, GithubContributor, on_replace: :delete) + field(:last_sync, :utc_datetime) + + field(:views, :integer, default: 0) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%CommunityCheatsheet{} = community_cheatsheet, attrs) do + community_cheatsheet + |> cast(attrs, @required_fields ++ @optional_fields) + |> cast_embed(:contributors, with: &GithubContributor.changeset/2) + |> validate_required(@required_fields) + end +end diff --git a/lib/mastani_server/cms/delegates/community_sync.ex b/lib/mastani_server/cms/delegates/community_sync.ex index 05917b881..267b4e6fe 100644 --- a/lib/mastani_server/cms/delegates/community_sync.ex +++ b/lib/mastani_server/cms/delegates/community_sync.ex @@ -10,7 +10,8 @@ defmodule MastaniServer.CMS.Delegate.CommunitySync do alias MastaniServer.CMS.{ Community, - CommunityWiki + CommunityWiki, + CommunityCheatsheet } @doc """ @@ -24,7 +25,17 @@ defmodule MastaniServer.CMS.Delegate.CommunitySync do end @doc """ - return paged community subscribers + get cheatsheet + """ + def get_cheatsheet(%Community{raw: raw}) do + with {:ok, community} <- ORM.find_by(Community, raw: raw), + {:ok, wiki} <- ORM.find_by(CommunityCheatsheet, community_id: community.id) do + CommunityCheatsheet |> ORM.read(wiki.id, inc: :views) + end + end + + @doc """ + sync wiki """ def sync_github_content(%Community{id: id}, :wiki, attrs) do with {:ok, community} <- ORM.find(Community, id) do @@ -34,6 +45,17 @@ defmodule MastaniServer.CMS.Delegate.CommunitySync do end end + @doc """ + sync cheatsheet + """ + def sync_github_content(%Community{id: id}, :cheatsheet, attrs) do + with {:ok, community} <- ORM.find(Community, id) do + attrs = Map.merge(attrs, %{community_id: community.id}) + + CommunityCheatsheet |> ORM.upsert_by([community_id: community.id], attrs) + end + end + @doc """ add contributor to exsit wiki contributors list """ @@ -41,6 +63,10 @@ defmodule MastaniServer.CMS.Delegate.CommunitySync do do_add_contributor(CommunityWiki, id, contributor_attrs) end + def add_contributor(%CommunityCheatsheet{id: id}, contributor_attrs) do + do_add_contributor(CommunityCheatsheet, id, contributor_attrs) + end + defp do_add_contributor(queryable, id, contributor_attrs) do with {:ok, content} <- ORM.find(queryable, id) do cur_contributors = diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 10bb3b986..26dfa9f34 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -259,4 +259,12 @@ defmodule MastaniServerWeb.Resolvers.CMS do def add_wiki_contributor(_root, ~m(id contributor)a, %{context: %{cur_user: _user}}) do CMS.add_contributor(%CMS.CommunityWiki{id: id}, contributor) end + + def sync_cheatsheet(_root, ~m(community_id readme last_sync)a, %{context: %{cur_user: _user}}) do + CMS.sync_github_content(%Community{id: community_id}, :cheatsheet, ~m(readme last_sync)a) + end + + def add_cheatsheet_contributor(_root, ~m(id contributor)a, %{context: %{cur_user: _user}}) do + CMS.add_contributor(%CMS.CommunityCheatsheet{id: id}, contributor) + end end diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index f0fcecb80..2acb900a4 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -41,6 +41,7 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do value(:video) value(:repo) value(:wiki) + value(:cheatsheet) end enum :order_enum do diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index c1a6b44af..e4e1c4010 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -343,6 +343,18 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:updated_at, :datetime) end + object :cheatsheet do + field(:id, :id) + field(:readme, :string) + field(:contributors, list_of(:github_contributor)) + + field(:last_sync, :datetime) + field(:views, :integer) + + field(:inserted_at, :datetime) + field(:updated_at, :datetime) + end + object :thread do field(:id, :id) field(:title, :string) diff --git a/lib/mastani_server_web/schema/cms/mutations/community.ex b/lib/mastani_server_web/schema/cms/mutations/community.ex index a60731c42..891c1e035 100644 --- a/lib/mastani_server_web/schema/cms/mutations/community.ex +++ b/lib/mastani_server_web/schema/cms/mutations/community.ex @@ -171,7 +171,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Community do resolve(&R.CMS.delete_tag/3) end - @desc "sync github docs like wiki / cheatsheets " + @desc "sync github wiki" field :sync_wiki, :wiki do arg(:community_id, non_null(:id)) arg(:readme, non_null(:string)) @@ -181,6 +181,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Community do resolve(&R.CMS.sync_wiki/3) end + @desc "add contributor to wiki " field :add_wiki_contributor, :wiki do arg(:id, non_null(:id)) arg(:contributor, non_null(:github_contributor_input)) @@ -188,5 +189,24 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Community do middleware(M.Authorize, :login) resolve(&R.CMS.add_wiki_contributor/3) end + + @desc "sync github cheatsheets " + field :sync_cheatsheet, :cheatsheet do + arg(:community_id, non_null(:id)) + arg(:readme, non_null(:string)) + arg(:last_sync, non_null(:datetime)) + + middleware(M.Authorize, :login) + resolve(&R.CMS.sync_cheatsheet/3) + end + + @desc "add contributor to cheatsheets" + field :add_cheatsheet_contributor, :cheatsheet do + arg(:id, non_null(:id)) + arg(:contributor, non_null(:github_contributor_input)) + + middleware(M.Authorize, :login) + resolve(&R.CMS.add_cheatsheet_contributor/3) + end end end diff --git a/priv/repo/migrations/20180930151113_create__community_cheatsheets.exs b/priv/repo/migrations/20180930151113_create__community_cheatsheets.exs new file mode 100644 index 000000000..1e069e773 --- /dev/null +++ b/priv/repo/migrations/20180930151113_create__community_cheatsheets.exs @@ -0,0 +1,18 @@ +defmodule MastaniServer.Repo.Migrations.CreateCommunityCheatsheets do + use Ecto.Migration + + def change do + create table(:community_cheatsheets) do + add(:community_id, references(:communities, on_delete: :delete_all), null: false) + add(:readme, :text) + # this should be a embed schema + add(:contributors, :map) + add(:last_sync, :utc_datetime) + add(:views, :integer, default: 0) + + timestamps() + end + + create(index(:community_cheatsheets, [:community_id])) + end +end diff --git a/test/mastani_server/cms/cheatsheet_test.exs b/test/mastani_server/cms/cheatsheet_test.exs new file mode 100644 index 000000000..90a47ac17 --- /dev/null +++ b/test/mastani_server/cms/cheatsheet_test.exs @@ -0,0 +1,69 @@ +defmodule MastaniServer.Test.Cheatsheet do + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.CMS + + alias CMS.{CommunityCheatsheet} + + setup do + {:ok, user} = db_insert(:user) + # {:ok, post} = db_insert(:post) + {:ok, community} = db_insert(:community) + + cheatsheet_attrs = mock_attrs(:cheatsheet, %{community_id: community.id}) + + {:ok, ~m(user community cheatsheet_attrs)a} + end + + describe "[cms cheatsheet sync]" do + @tag :wip + test "can create create/sync a cheatsheet to a community", ~m(community cheatsheet_attrs)a do + {:ok, cheatsheet} = CMS.sync_github_content(community, :cheatsheet, cheatsheet_attrs) + + assert cheatsheet.community_id == community.id + assert cheatsheet.last_sync == cheatsheet_attrs.last_sync + end + + @tag :wip + test "can update a exsit cheatsheet", ~m(community cheatsheet_attrs)a do + {:ok, cheatsheet} = CMS.sync_github_content(community, :cheatsheet, cheatsheet_attrs) + + new_cheatsheet_attrs = + mock_attrs(:cheatsheet, %{community_id: community.id, readme: "new readme"}) + + {:ok, _} = CMS.sync_github_content(community, :cheatsheet, new_cheatsheet_attrs) + {:ok, new_cheatsheet} = CommunityCheatsheet |> ORM.find(cheatsheet.id) + + assert new_cheatsheet.readme == "new readme" + end + + @tag :wip + test "can add contributor to cheatsheet", ~m(community cheatsheet_attrs)a do + {:ok, cheatsheet} = CMS.sync_github_content(community, :cheatsheet, cheatsheet_attrs) + cur_contributors = cheatsheet.contributors + + contributor_attrs = mock_attrs(:github_contributor) + {:ok, cheatsheet} = CMS.add_contributor(cheatsheet, contributor_attrs) + update_contributors = cheatsheet.contributors + + assert length(update_contributors) == 1 + length(cur_contributors) + end + + @tag :wip + test "add some contributor fails", ~m(community cheatsheet_attrs)a do + {:ok, cheatsheet} = CMS.sync_github_content(community, :cheatsheet, cheatsheet_attrs) + cur_contributors = cheatsheet.contributors + + contributor_attrs = mock_attrs(:github_contributor) + {:ok, cheatsheet} = CMS.add_contributor(cheatsheet, contributor_attrs) + update_contributors = cheatsheet.contributors + + assert length(update_contributors) == 1 + length(cur_contributors) + + # add some again + {:error, error} = CMS.add_contributor(cheatsheet, contributor_attrs) + assert error |> Keyword.get(:code) == ecode(:already_exsit) + end + end +end diff --git a/test/mastani_server_web/mutation/cms/cheatsheet_test.exs b/test/mastani_server_web/mutation/cms/cheatsheet_test.exs new file mode 100644 index 000000000..3b265b024 --- /dev/null +++ b/test/mastani_server_web/mutation/cms/cheatsheet_test.exs @@ -0,0 +1,91 @@ +defmodule MastaniServer.Test.Mutation.CMS.Cheatsheet do + use MastaniServer.TestTools + + alias MastaniServer.CMS + alias MastaniServer.Statistics + + alias CMS.{Community} + + alias Helper.ORM + + setup do + {:ok, user} = db_insert(:user) + {:ok, community} = db_insert(:community) + + cheatsheet_attrs = mock_attrs(:cheatsheet, %{community_id: community.id}) + {:ok, cheatsheet} = CMS.sync_github_content(community, :cheatsheet, cheatsheet_attrs) + + user_conn = simu_conn(:user) + guest_conn = simu_conn(:guest) + + {:ok, ~m(user_conn guest_conn community user cheatsheet)a} + end + + @sync_cheatsheet_query """ + mutation($communityId: ID!, $readme: String!, $lastSync: String!){ + syncCheatsheet(communityId: $communityId, readme: $readme, lastSync: $lastSync) { + id + readme + } + } + """ + @tag :wip + test "login user can sync cheatsheet", ~m(community)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + cheatsheet_attrs = mock_attrs(:cheatsheet) + + variables = cheatsheet_attrs |> Map.merge(%{communityId: community.id}) + created = user_conn |> mutation_result(@sync_cheatsheet_query, variables, "syncCheatsheet") + + {:ok, cheatsheet} = ORM.find(CMS.CommunityCheatsheet, created["id"]) + + assert created["id"] == to_string(cheatsheet.id) + assert created["readme"] == to_string(cheatsheet.readme) + end + + @add_cheatsheet_contribotor_query """ + mutation($id: ID!, $contributor: GithubContributorInput!){ + addCheatsheetContributor(id: $id, contributor: $contributor) { + id + readme + contributors { + nickname + } + } + } + """ + test "login user can add contributor to an exsit cheatsheet", ~m(user cheatsheet)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + contributor_attrs = mock_attrs(:github_contributor) + variables = %{id: cheatsheet.id, contributor: contributor_attrs} + + created = + user_conn + |> mutation_result(@add_cheatsheet_contribotor_query, variables, "addCheatsheetContributor") + + assert created["contributors"] |> length == 4 + end + + test "add some contributor fails", ~m(user cheatsheet)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + contributor_attrs = mock_attrs(:github_contributor) + variables = %{id: cheatsheet.id, contributor: contributor_attrs} + + created = + user_conn + |> mutation_result(@add_cheatsheet_contribotor_query, variables, "addCheatshhetContributor") + + assert user_conn + |> mutation_get_error?( + @add_cheatsheet_contribotor_query, + variables, + ecode(:already_exsit) + ) + end +end diff --git a/test/mastani_server_web/mutation/cms/repo_test.exs b/test/mastani_server_web/mutation/cms/repo_test.exs index 721375b6c..ab668abea 100644 --- a/test/mastani_server_web/mutation/cms/repo_test.exs +++ b/test/mastani_server_web/mutation/cms/repo_test.exs @@ -62,6 +62,7 @@ defmodule MastaniServer.Test.Mutation.Repo do } } """ + @tag :wip2 test "create repo with valid attrs and make sure author exsit" do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) diff --git a/test/support/factory.ex b/test/support/factory.ex index 9fa46aa2c..512ddd70f 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -94,8 +94,8 @@ defmodule MastaniServer.Support.Factory do color: "tomato" }, contributors: [ - mock_meta(:github_contributor), - mock_meta(:github_contributor) + mock_meta(:repo_contributor), + mock_meta(:repo_contributor) ], author: mock(:author), views: Enum.random(0..2000), @@ -120,6 +120,19 @@ defmodule MastaniServer.Support.Factory do } end + defp mock_meta(:cheatsheet) do + mock_meta(:wiki) + end + + defp mock_meta(:repo_contributor) do + %{ + avatar: Faker.Avatar.image_url(), + html_url: Faker.Avatar.image_url(), + htmlUrl: Faker.Avatar.image_url(), + nickname: "mydearxym2" + } + end + defp mock_meta(:github_contributor) do unique_num = System.unique_integer([:positive, :monotonic]) @@ -280,6 +293,7 @@ defmodule MastaniServer.Support.Factory do def mock_attrs(:mention, attrs), do: mock_meta(:mention) |> Map.merge(attrs) def mock_attrs(:wiki, attrs), do: mock_meta(:wiki) |> Map.merge(attrs) + def mock_attrs(:cheatsheet, attrs), do: mock_meta(:cheatsheet) |> Map.merge(attrs) def mock_attrs(:github_contributor, attrs), do: mock_meta(:github_contributor) |> Map.merge(attrs) @@ -305,6 +319,7 @@ defmodule MastaniServer.Support.Factory do defp mock(:repo), do: CMS.Repo |> struct(mock_meta(:repo)) defp mock(:job), do: CMS.Job |> struct(mock_meta(:job)) defp mock(:wiki), do: CMS.CommunityWiki |> struct(mock_meta(:wiki)) + defp mock(:cheatsheet), do: CMS.CommunityCheatsheet |> struct(mock_meta(:cheatsheet)) defp mock(:comment), do: CMS.Comment |> struct(mock_meta(:comment)) defp mock(:mention), do: Delivery.Mention |> struct(mock_meta(:mention)) defp mock(:author), do: CMS.Author |> struct(mock_meta(:author)) From 8136cced4e1b7ca2ac486ecd95a47430c6f07f40 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 30 Sep 2018 23:44:18 +0800 Subject: [PATCH 033/129] chore(clean up): warnings and wip tag --- test/mastani_server/cms/cheatsheet_test.exs | 4 ---- .../mutation/cms/cheatsheet_test.exs | 16 +++++----------- .../mutation/cms/repo_test.exs | 1 - .../mutation/cms/wiki_test.exs | 13 ++++--------- 4 files changed, 9 insertions(+), 25 deletions(-) diff --git a/test/mastani_server/cms/cheatsheet_test.exs b/test/mastani_server/cms/cheatsheet_test.exs index 90a47ac17..b242eefeb 100644 --- a/test/mastani_server/cms/cheatsheet_test.exs +++ b/test/mastani_server/cms/cheatsheet_test.exs @@ -17,7 +17,6 @@ defmodule MastaniServer.Test.Cheatsheet do end describe "[cms cheatsheet sync]" do - @tag :wip test "can create create/sync a cheatsheet to a community", ~m(community cheatsheet_attrs)a do {:ok, cheatsheet} = CMS.sync_github_content(community, :cheatsheet, cheatsheet_attrs) @@ -25,7 +24,6 @@ defmodule MastaniServer.Test.Cheatsheet do assert cheatsheet.last_sync == cheatsheet_attrs.last_sync end - @tag :wip test "can update a exsit cheatsheet", ~m(community cheatsheet_attrs)a do {:ok, cheatsheet} = CMS.sync_github_content(community, :cheatsheet, cheatsheet_attrs) @@ -38,7 +36,6 @@ defmodule MastaniServer.Test.Cheatsheet do assert new_cheatsheet.readme == "new readme" end - @tag :wip test "can add contributor to cheatsheet", ~m(community cheatsheet_attrs)a do {:ok, cheatsheet} = CMS.sync_github_content(community, :cheatsheet, cheatsheet_attrs) cur_contributors = cheatsheet.contributors @@ -50,7 +47,6 @@ defmodule MastaniServer.Test.Cheatsheet do assert length(update_contributors) == 1 + length(cur_contributors) end - @tag :wip test "add some contributor fails", ~m(community cheatsheet_attrs)a do {:ok, cheatsheet} = CMS.sync_github_content(community, :cheatsheet, cheatsheet_attrs) cur_contributors = cheatsheet.contributors diff --git a/test/mastani_server_web/mutation/cms/cheatsheet_test.exs b/test/mastani_server_web/mutation/cms/cheatsheet_test.exs index 3b265b024..38c21230b 100644 --- a/test/mastani_server_web/mutation/cms/cheatsheet_test.exs +++ b/test/mastani_server_web/mutation/cms/cheatsheet_test.exs @@ -1,12 +1,8 @@ defmodule MastaniServer.Test.Mutation.CMS.Cheatsheet do use MastaniServer.TestTools - alias MastaniServer.CMS - alias MastaniServer.Statistics - - alias CMS.{Community} - alias Helper.ORM + alias MastaniServer.CMS setup do {:ok, user} = db_insert(:user) @@ -29,7 +25,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Cheatsheet do } } """ - @tag :wip test "login user can sync cheatsheet", ~m(community)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -56,7 +51,7 @@ defmodule MastaniServer.Test.Mutation.CMS.Cheatsheet do } } """ - test "login user can add contributor to an exsit cheatsheet", ~m(user cheatsheet)a do + test "login user can add contributor to an exsit cheatsheet", ~m(cheatsheet)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -70,16 +65,15 @@ defmodule MastaniServer.Test.Mutation.CMS.Cheatsheet do assert created["contributors"] |> length == 4 end - test "add some contributor fails", ~m(user cheatsheet)a do + test "add some contributor fails", ~m(cheatsheet)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) contributor_attrs = mock_attrs(:github_contributor) variables = %{id: cheatsheet.id, contributor: contributor_attrs} - created = - user_conn - |> mutation_result(@add_cheatsheet_contribotor_query, variables, "addCheatshhetContributor") + user_conn + |> mutation_result(@add_cheatsheet_contribotor_query, variables, "addCheatshhetContributor") assert user_conn |> mutation_get_error?( diff --git a/test/mastani_server_web/mutation/cms/repo_test.exs b/test/mastani_server_web/mutation/cms/repo_test.exs index ab668abea..721375b6c 100644 --- a/test/mastani_server_web/mutation/cms/repo_test.exs +++ b/test/mastani_server_web/mutation/cms/repo_test.exs @@ -62,7 +62,6 @@ defmodule MastaniServer.Test.Mutation.Repo do } } """ - @tag :wip2 test "create repo with valid attrs and make sure author exsit" do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) diff --git a/test/mastani_server_web/mutation/cms/wiki_test.exs b/test/mastani_server_web/mutation/cms/wiki_test.exs index 453343c7d..7cbe3ef84 100644 --- a/test/mastani_server_web/mutation/cms/wiki_test.exs +++ b/test/mastani_server_web/mutation/cms/wiki_test.exs @@ -1,12 +1,8 @@ defmodule MastaniServer.Test.Mutation.CMS.Wiki do use MastaniServer.TestTools - alias MastaniServer.CMS - alias MastaniServer.Statistics - - alias CMS.{Community} - alias Helper.ORM + alias MastaniServer.CMS setup do {:ok, user} = db_insert(:user) @@ -55,7 +51,7 @@ defmodule MastaniServer.Test.Mutation.CMS.Wiki do } } """ - test "login user can add contributor to an exsit wiki", ~m(user wiki)a do + test "login user can add contributor to an exsit wiki", ~m(wiki)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -68,15 +64,14 @@ defmodule MastaniServer.Test.Mutation.CMS.Wiki do assert created["contributors"] |> length == 4 end - test "add some contributor fails", ~m(user wiki)a do + test "add some contributor fails", ~m(wiki)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) contributor_attrs = mock_attrs(:github_contributor) variables = %{id: wiki.id, contributor: contributor_attrs} - created = - user_conn |> mutation_result(@add_wiki_contribotor_query, variables, "addWikiContributor") + user_conn |> mutation_result(@add_wiki_contribotor_query, variables, "addWikiContributor") assert user_conn |> mutation_get_error?(@add_wiki_contribotor_query, variables, ecode(:already_exsit)) From 05805e0d8f22a2e37b7303e7a0a13535011d9030 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 1 Oct 2018 09:23:01 +0800 Subject: [PATCH 034/129] refactor(factory): auto camelize factory attrs if need by remove the hand-code camelize the attrs to use for Graphql query args --- lib/helper/utils.ex | 33 +++++++++++++++++++ mix.exs | 3 +- mix.lock | 1 + .../mutation/cms/repo_test.exs | 3 +- .../mutation/cms/video_test.exs | 2 +- .../mutation/cms/wiki_test.exs | 3 +- test/support/factory.ex | 22 ------------- test/support/test_tools.ex | 1 + 8 files changed, 41 insertions(+), 27 deletions(-) diff --git a/lib/helper/utils.ex b/lib/helper/utils.ex index f1fd15466..f280cad26 100644 --- a/lib/helper/utils.ex +++ b/lib/helper/utils.ex @@ -69,6 +69,39 @@ defmodule Helper.Utils do map |> Enum.reduce(%{}, fn {key, val}, acc -> Map.put(acc, to_string(key), val) end) end + @doc """ + Recursivly camelize the map keys + usage: convert factory attrs to used for simu Graphql parmas + """ + def camelize_map_key(map) do + map_list = + Enum.map(map, fn {k, v} -> + v = + cond do + is_datetime?(v) -> + DateTime.to_iso8601(v) + + is_map(v) -> + camelize_map_key(safe_map(v)) + + true -> + v + end + + map_to_camel({k, v}) + end) + + Enum.into(map_list, %{}) + end + + defp safe_map(%{__struct__: _} = map), do: Map.from_struct(map) + defp safe_map(map), do: map + + defp map_to_camel({k, v}), do: {Recase.to_camel(to_string(k)), v} + + def is_datetime?(%DateTime{}), do: true + def is_datetime?(_), do: false + def deep_merge(left, right) do Map.merge(left, right, &deep_resolve/3) end diff --git a/mix.exs b/mix.exs index ea5a12839..33119d71a 100644 --- a/mix.exs +++ b/mix.exs @@ -88,7 +88,8 @@ defmodule MastaniServer.Mixfile do {:credo, "~> 0.10.0", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.0.0-rc.2", only: [:dev, :mock], runtime: false}, {:excoveralls, "~> 0.8", only: :test}, - {:sentry, "~> 6.4"} + {:sentry, "~> 6.4"}, + {:recase, "~> 0.3.0"} ] end diff --git a/mix.lock b/mix.lock index dfbddeef5..990310302 100644 --- a/mix.lock +++ b/mix.lock @@ -47,6 +47,7 @@ "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, "pre_commit": {:hex, :pre_commit, "0.3.4", "e2850f80be8090d50ad8019ef2426039307ff5dfbe70c736ad0d4d401facf304", [:mix], [], "hexpm"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, + "recase": {:hex, :recase, "0.3.0", "a3a6b2bfc9a1c3047b6f37d49ea52027ea59fd256505254b8e9d63c68d09ab89", [:mix], [], "hexpm"}, "scrivener": {:git, "https://github.com/mastani-stack/scrivener", "dc603c5cdf884c4fe33b2b09d5672ab6be3e2c14", []}, "scrivener_ecto": {:git, "https://github.com/mastani-stack/scrivener_ecto", "e7d2f287c9189f2aebf478724b6c276694411c92", []}, "sentry": {:hex, :sentry, "6.4.1", "882287f1f3167dc4794865124977e2d88878d51d19930c0d0e7cc3a663a4a181", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/mastani_server_web/mutation/cms/repo_test.exs b/test/mastani_server_web/mutation/cms/repo_test.exs index 721375b6c..04cefdd49 100644 --- a/test/mastani_server_web/mutation/cms/repo_test.exs +++ b/test/mastani_server_web/mutation/cms/repo_test.exs @@ -67,12 +67,11 @@ defmodule MastaniServer.Test.Mutation.Repo do user_conn = simu_conn(:user, user) {:ok, community} = db_insert(:community) - repo_attr = mock_attrs(:repo) + repo_attr = mock_attrs(:repo) |> camelize_map_key variables = repo_attr |> Map.merge(%{communityId: community.id}) created = user_conn |> mutation_result(@create_repo_query, variables, "createRepo") {:ok, repo} = ORM.find(CMS.Repo, created["id"]) - # IO.inspect repo, label: "hello" assert created["id"] == to_string(repo.id) assert {:ok, _} = ORM.find_by(CMS.Author, user_id: user.id) diff --git a/test/mastani_server_web/mutation/cms/video_test.exs b/test/mastani_server_web/mutation/cms/video_test.exs index e8c1ce8ca..2554470ed 100644 --- a/test/mastani_server_web/mutation/cms/video_test.exs +++ b/test/mastani_server_web/mutation/cms/video_test.exs @@ -57,7 +57,7 @@ defmodule MastaniServer.Test.Mutation.Video do user_conn = simu_conn(:user, user) {:ok, community} = db_insert(:community) - video_attr = mock_attrs(:video) + video_attr = mock_attrs(:video) |> camelize_map_key variables = video_attr |> Map.merge(%{communityId: community.id}) created = user_conn |> mutation_result(@create_video_query, variables, "createVideo") diff --git a/test/mastani_server_web/mutation/cms/wiki_test.exs b/test/mastani_server_web/mutation/cms/wiki_test.exs index 7cbe3ef84..8440499d6 100644 --- a/test/mastani_server_web/mutation/cms/wiki_test.exs +++ b/test/mastani_server_web/mutation/cms/wiki_test.exs @@ -29,7 +29,8 @@ defmodule MastaniServer.Test.Mutation.CMS.Wiki do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) - wiki_attrs = mock_attrs(:wiki) + # IO.inspect(mock_attrs(:wiki) |> camelize_map_key, label: "hello ...") + wiki_attrs = mock_attrs(:wiki) |> camelize_map_key variables = wiki_attrs |> Map.merge(%{communityId: community.id}) created = user_conn |> mutation_result(@sync_wiki_query, variables, "syncWiki") diff --git a/test/support/factory.ex b/test/support/factory.ex index 512ddd70f..8b7ff1e4b 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -37,17 +37,13 @@ defmodule MastaniServer.Support.Factory do desc: desc, duration: "03:30", duration_sec: Enum.random(300..12_000), - durationSec: Enum.random(300..12_000), source: "youtube", link: "http://www.youtube.com/video/1", original_author: "mydearxym", - originalAuthor: "mydearxym", original_author_link: "http://www.youtube.com/user/1", - originalAuthorLink: "http://www.youtube.com/user/1", author: mock(:author), views: Enum.random(0..2000), publish_at: Timex.today() |> Timex.to_datetime(), - publishAt: "2017-11-01T12:00:00Z", communities: [ mock(:community), mock(:community) @@ -61,38 +57,23 @@ defmodule MastaniServer.Support.Factory do %{ title: Faker.Lorem.Shakespeare.king_richard_iii(), owner_name: "coderplanets", - ownerName: "coderplanets", owner_url: "http://www.github.com/coderplanets", - ownerUrl: "http://www.github.com/coderplanets", repo_url: "http://www.github.com/coderplanets//coderplanets_server", - repoUrl: "http://www.github.com/coderplanets//coderplanets_server", desc: desc, homepage_url: "http://www.github.com/coderplanets", - homepageUrl: "http://www.github.com/coderplanets", readme: desc, issues_count: Enum.random(0..2000), - issuesCount: Enum.random(0..2000), prs_count: Enum.random(0..2000), - prsCount: Enum.random(0..2000), fork_count: Enum.random(0..2000), - forkCount: Enum.random(0..2000), star_count: Enum.random(0..2000), - starCount: Enum.random(0..2000), watch_count: Enum.random(0..2000), - watchCount: Enum.random(0..2000), primary_language: "javascript", - primaryLanguage: "javascript", license: "MIT", release_tag: "v22", - releaseTag: "v22", primary_language: %{ name: "javascript", color: "tomato" }, - primaryLanguage: %{ - name: "javascript", - color: "tomato" - }, contributors: [ mock_meta(:repo_contributor), mock_meta(:repo_contributor) @@ -111,7 +92,6 @@ defmodule MastaniServer.Support.Factory do community: mock(:community), readme: Faker.Lorem.sentence(%Range{first: 15, last: 60}), last_sync: Timex.today() |> Timex.to_datetime(), - lastSync: "2017-11-01T12:00:00Z", contributors: [ mock_meta(:github_contributor), mock_meta(:github_contributor), @@ -138,10 +118,8 @@ defmodule MastaniServer.Support.Factory do %{ github_id: "#{unique_num}-#{Faker.Lorem.sentence(%Range{first: 5, last: 10})}", - githubId: "#{unique_num}-#{Faker.Lorem.sentence(%Range{first: 5, last: 10})}", avatar: Faker.Avatar.image_url(), html_url: Faker.Avatar.image_url(), - htmlUrl: Faker.Avatar.image_url(), nickname: "mydearxym2", bio: Faker.Lorem.sentence(%Range{first: 15, last: 60}), location: "location #{unique_num}", diff --git a/test/support/test_tools.ex b/test/support/test_tools.ex index ef77bc2b7..0b0d84218 100644 --- a/test/support/test_tools.ex +++ b/test/support/test_tools.ex @@ -13,6 +13,7 @@ defmodule MastaniServer.TestTools do import MastaniServer.Test.AssertHelper import Ecto.Query, warn: false import Helper.ErrorCode + import Helper.Utils, only: [camelize_map_key: 1] import ShortMaps end From 266aa534c82dee1c99abf0e7f43e8ad6d62f3930 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 2 Oct 2018 09:46:05 +0800 Subject: [PATCH 035/129] feat(json encode): config Jason in absinthe , ecto --- config/config.exs | 1 + lib/mastani_server_web/router.ex | 1 + mix.exs | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 1d0168e20..3bf9a1674 100644 --- a/config/config.exs +++ b/config/config.exs @@ -21,6 +21,7 @@ config :logger, :console, metadata: [:request_id] config :phoenix, :format_encoders, json: Jason +config :ecto, json_library: Jason # TODO move this config to secret later config :mastani_server, Helper.Guardian, diff --git a/lib/mastani_server_web/router.ex b/lib/mastani_server_web/router.ex index 18669fb10..3a8d69c32 100644 --- a/lib/mastani_server_web/router.ex +++ b/lib/mastani_server_web/router.ex @@ -16,6 +16,7 @@ defmodule MastaniServerWeb.Router do "/", Absinthe.Plug.GraphiQL, schema: MastaniServerWeb.Schema, + json_codec: Jason, pipeline: {ApolloTracing.Pipeline, :plug}, interface: :playground, context: %{pubsub: MastaniServerWeb.Endpoint} diff --git a/mix.exs b/mix.exs index 33119d71a..964860ac2 100644 --- a/mix.exs +++ b/mix.exs @@ -84,7 +84,7 @@ defmodule MastaniServer.Mixfile do {:pre_commit, "~> 0.3.4"}, {:inch_ex, "~> 1.0", only: [:dev, :test]}, {:short_maps, "~> 0.1.1"}, - {:jason, "~> 1.0"}, + {:jason, "~> 1.1.1"}, {:credo, "~> 0.10.0", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.0.0-rc.2", only: [:dev, :mock], runtime: false}, {:excoveralls, "~> 0.8", only: :test}, From 00fb4da12bcaa8c8bbbff8f355a91c304c198169 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 2 Oct 2018 09:46:42 +0800 Subject: [PATCH 036/129] refactor(test): clean up warning --- lib/helper/radar_search.ex | 2 +- .../accounts/delegates/favorite_category.ex | 4 +- .../accounts/delegates/profile.ex | 4 +- .../accounts/favorite_category.ex | 2 +- .../cms/delegates/community_sync.ex | 2 +- lib/mastani_server/cms/utils/matcher.ex | 2 +- .../schema/statistics/statistics_types.ex | 2 +- .../mutation/accounts/account_test.exs | 46 ++++++++++--------- .../mutation/cms/cheatsheet_test.exs | 2 +- test/support/assert_helper.ex | 1 + test/support/factory.ex | 1 - 11 files changed, 36 insertions(+), 32 deletions(-) diff --git a/lib/helper/radar_search.ex b/lib/helper/radar_search.ex index ce5524f89..c5df77395 100644 --- a/lib/helper/radar_search.ex +++ b/lib/helper/radar_search.ex @@ -37,7 +37,7 @@ defmodule Helper.RadarSearch do {:error, "error"} end else - error -> + _ -> {:ok, "成都"} # {:error, "error"} end diff --git a/lib/mastani_server/accounts/delegates/favorite_category.ex b/lib/mastani_server/accounts/delegates/favorite_category.ex index e0716f965..1fcda8639 100644 --- a/lib/mastani_server/accounts/delegates/favorite_category.ex +++ b/lib/mastani_server/accounts/delegates/favorite_category.ex @@ -53,7 +53,7 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do defp delete_favorites_result({:ok, %{delete_favorite_record: result}}), do: {:ok, result} - defp delete_favorites_result({:error, :delete_category, result, _steps}) do + defp delete_favorites_result({:error, :delete_category, _result, _steps}) do {:error, [message: "delete category fails", code: ecode(:delete_fails)]} end @@ -64,7 +64,7 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do def list_favorite_categories( %User{id: user_id}, %{private: private}, - %{page: page, size: size} = filter + %{page: page, size: size} ) do query = case private do diff --git a/lib/mastani_server/accounts/delegates/profile.ex b/lib/mastani_server/accounts/delegates/profile.ex index eb840f0ca..309c9f601 100644 --- a/lib/mastani_server/accounts/delegates/profile.ex +++ b/lib/mastani_server/accounts/delegates/profile.ex @@ -102,7 +102,9 @@ defmodule MastaniServer.Accounts.Delegate.Profile do # ignore error case RadarSearch.locate_city(remote_ip) do {:ok, city} -> update_profile(user, %{geo_city: city}) - {:error, _} -> IO.inspect("location search error") + {:error, _} -> + # IO.inspect("location search error") + {:ok, "pass"} end token_info(user) diff --git a/lib/mastani_server/accounts/favorite_category.ex b/lib/mastani_server/accounts/favorite_category.ex index 9da0ecfcd..25bd35bac 100644 --- a/lib/mastani_server/accounts/favorite_category.ex +++ b/lib/mastani_server/accounts/favorite_category.ex @@ -11,7 +11,7 @@ defmodule MastaniServer.Accounts.FavoriteCategory do @type t :: %FavoriteCategory{} schema "favorite_categories" do - belongs_to(:user, Accounts.User, foreign_key: :user_id) + belongs_to(:user, User, foreign_key: :user_id) # has_many(:posts, ...) field(:title, :string) diff --git a/lib/mastani_server/cms/delegates/community_sync.ex b/lib/mastani_server/cms/delegates/community_sync.ex index 267b4e6fe..0dd0ed711 100644 --- a/lib/mastani_server/cms/delegates/community_sync.ex +++ b/lib/mastani_server/cms/delegates/community_sync.ex @@ -4,7 +4,7 @@ defmodule MastaniServer.CMS.Delegate.CommunitySync do """ import Ecto.Query, warn: false import Helper.ErrorCode - import ShortMaps + # import ShortMaps alias Helper.ORM diff --git a/lib/mastani_server/cms/utils/matcher.ex b/lib/mastani_server/cms/utils/matcher.ex index 45e3c350d..5f7598aa4 100644 --- a/lib/mastani_server/cms/utils/matcher.ex +++ b/lib/mastani_server/cms/utils/matcher.ex @@ -81,7 +81,7 @@ defmodule MastaniServer.CMS.Utils.Matcher do def match_action(:job, :self), do: {:ok, %{target: Job, reactor: Job, preload: :author}} def match_action(:job, :community), - do: {:ok, %{target: Job, reactor: Community, flag: JobCommunityFlags}} + do: {:ok, %{target: Job, reactor: Community, flag: JobCommunityFlag}} def match_action(:job, :star), do: {:ok, %{target: Job, reactor: JobStar, preload: :user}} def match_action(:job, :tag), do: {:ok, %{target: Job, reactor: Tag}} diff --git a/lib/mastani_server_web/schema/statistics/statistics_types.ex b/lib/mastani_server_web/schema/statistics/statistics_types.ex index 5332f4c64..848be61b3 100644 --- a/lib/mastani_server_web/schema/statistics/statistics_types.ex +++ b/lib/mastani_server_web/schema/statistics/statistics_types.ex @@ -2,7 +2,7 @@ defmodule MastaniServerWeb.Schema.Statistics.Types do use Absinthe.Schema.Notation use Absinthe.Ecto, repo: MastaniServer.Repo - import MastaniServerWeb.Schema.Utils.Helper + # import MastaniServerWeb.Schema.Utils.Helper # alias MastaniServer.Accounts diff --git a/test/mastani_server_web/mutation/accounts/account_test.exs b/test/mastani_server_web/mutation/accounts/account_test.exs index 576a7b052..5a3ad4a65 100644 --- a/test/mastani_server_web/mutation/accounts/account_test.exs +++ b/test/mastani_server_web/mutation/accounts/account_test.exs @@ -91,43 +91,45 @@ defmodule MastaniServer.Test.Mutation.Account.Basic do assert updated["work_backgrounds"] |> Enum.any?(&(&1["title"] == "CTO")) end + @tag :wip test "user update education_backgrounds with invalid data fails", ~m(user)a do ownd_conn = simu_conn(:user, user) variables = %{ profile: %{ - nickname: "new nickname", - education_backgrounds: [ - %{ - major: "bad ass2" - }, - %{ - school: "school2", - major: "bad ass2" - } - ] - } + nickname: "new nickname" + }, + educationBackgrounds: [ + %{ + major: "bad ass2" + }, + %{ + school: "school2", + major: "bad ass2" + } + ] } assert ownd_conn |> mutation_get_error?(@update_query, variables) end + @tag :wip test "user update work backgrounds with invalid data fails", ~m(user)a do ownd_conn = simu_conn(:user, user) variables = %{ profile: %{ - nickname: "new nickname", - work_backgrounds: [ - %{ - title: "bad ass2" - }, - %{ - company: "school2", - title: "bad ass2" - } - ] - } + nickname: "new nickname" + }, + workBackgrounds: [ + %{ + title: "bad ass2" + }, + %{ + company: "school2", + title: "bad ass2" + } + ] } assert ownd_conn |> mutation_get_error?(@update_query, variables) diff --git a/test/mastani_server_web/mutation/cms/cheatsheet_test.exs b/test/mastani_server_web/mutation/cms/cheatsheet_test.exs index 38c21230b..aa3e877cf 100644 --- a/test/mastani_server_web/mutation/cms/cheatsheet_test.exs +++ b/test/mastani_server_web/mutation/cms/cheatsheet_test.exs @@ -29,7 +29,7 @@ defmodule MastaniServer.Test.Mutation.CMS.Cheatsheet do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) - cheatsheet_attrs = mock_attrs(:cheatsheet) + cheatsheet_attrs = mock_attrs(:cheatsheet) |> camelize_map_key variables = cheatsheet_attrs |> Map.merge(%{communityId: community.id}) created = user_conn |> mutation_result(@sync_cheatsheet_query, variables, "syncCheatsheet") diff --git a/test/support/assert_helper.ex b/test/support/assert_helper.ex index bc4a3000e..d2aec7994 100644 --- a/test/support/assert_helper.ex +++ b/test/support/assert_helper.ex @@ -80,6 +80,7 @@ defmodule MastaniServer.Test.AssertHelper do def mutation_get_error?(conn, query, variables) do conn |> post("/graphiql", query: query, variables: variables) + # |> IO.inspect(label: "debug status") |> json_response(200) # |> IO.inspect(label: "debug") |> Map.has_key?("errors") diff --git a/test/support/factory.ex b/test/support/factory.ex index 8b7ff1e4b..4ba456e31 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -67,7 +67,6 @@ defmodule MastaniServer.Support.Factory do fork_count: Enum.random(0..2000), star_count: Enum.random(0..2000), watch_count: Enum.random(0..2000), - primary_language: "javascript", license: "MIT", release_tag: "v22", primary_language: %{ From b250f5c9de8df7c64754e6480d61178184d6d9d1 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 2 Oct 2018 09:47:42 +0800 Subject: [PATCH 037/129] style: fmt --- lib/mastani_server/accounts/delegates/profile.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/mastani_server/accounts/delegates/profile.ex b/lib/mastani_server/accounts/delegates/profile.ex index 309c9f601..b739b2413 100644 --- a/lib/mastani_server/accounts/delegates/profile.ex +++ b/lib/mastani_server/accounts/delegates/profile.ex @@ -101,9 +101,11 @@ defmodule MastaniServer.Accounts.Delegate.Profile do defp register_github_result({:ok, %{create_user: user}}, remote_ip) do # ignore error case RadarSearch.locate_city(remote_ip) do - {:ok, city} -> update_profile(user, %{geo_city: city}) + {:ok, city} -> + update_profile(user, %{geo_city: city}) + + # IO.inspect("location search error") {:error, _} -> - # IO.inspect("location search error") {:ok, "pass"} end From b8ff8dfa6587445beb2ec253dd745f13edc07c12 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 2 Oct 2018 23:07:24 +0800 Subject: [PATCH 038/129] fix: missing field --- lib/mastani_server_web/schema/cms/cms_types.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index e4e1c4010..5950e2b8d 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -325,6 +325,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do object :github_contributor do field(:avatar, :string) + field(:bio, :string) field(:html_url, :string) field(:nickname, :string) field(:location, :string) From 04d7ac4c4362293311c063314051fc5b40331c2d Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 7 Oct 2018 20:38:19 +0800 Subject: [PATCH 039/129] refactor(reactions): extract comment fields and fix job reaction --- lib/helper/query_builder.ex | 6 ++ lib/helper/utils.ex | 1 + .../cms/delegates/article_reaction.ex | 10 ++- lib/mastani_server/cms/job.ex | 13 ++- lib/mastani_server/cms/utils/loader.ex | 21 +++-- .../resolvers/cms_resolver.ex | 5 +- lib/mastani_server_web/schema/cms/cms_misc.ex | 5 +- .../schema/cms/cms_queries.ex | 6 ++ .../schema/cms/cms_types.ex | 87 +++++-------------- lib/mastani_server_web/schema/utils/helper.ex | 80 +++++++++++++++++ .../query/cms/cheatsheet_test.exs | 55 ++++++++++++ 11 files changed, 206 insertions(+), 83 deletions(-) create mode 100644 test/mastani_server_web/query/cms/cheatsheet_test.exs diff --git a/lib/helper/query_builder.ex b/lib/helper/query_builder.ex index 1ffffda47..95979b8c5 100644 --- a/lib/helper/query_builder.ex +++ b/lib/helper/query_builder.ex @@ -27,6 +27,12 @@ defmodule Helper.QueryBuilder do |> select([f], count(f.id)) end + def members_pack(queryable, %{count: _, type: :job}) do + queryable + |> group_by([f], f.job_id) + |> select([f], count(f.id)) + end + def members_pack(queryable, %{count: _, type: :community}) do queryable |> group_by([f], f.community_id) diff --git a/lib/helper/utils.ex b/lib/helper/utils.ex index f280cad26..e0bcb7585 100644 --- a/lib/helper/utils.ex +++ b/lib/helper/utils.ex @@ -23,6 +23,7 @@ defmodule Helper.Utils do def done(_, :boolean), do: {:ok, true} def done(nil, err_msg), do: {:error, err_msg} def done({:ok, _}, with: result), do: {:ok, result} + def done({:error, error}, with: _result), do: {:error, error} def done({:ok, %{id: id}}, :status), do: {:ok, %{done: true, id: id}} def done({:error, _}, :status), do: {:ok, %{done: false}} diff --git a/lib/mastani_server/cms/delegates/article_reaction.ex b/lib/mastani_server/cms/delegates/article_reaction.ex index af1f290cb..218808d50 100644 --- a/lib/mastani_server/cms/delegates/article_reaction.ex +++ b/lib/mastani_server/cms/delegates/article_reaction.ex @@ -36,8 +36,9 @@ defmodule MastaniServer.CMS.Delegate.ArticleReaction do defp reaction_result({:ok, %{create_reaction_record: result}}), do: result |> done() - defp reaction_result({:error, :create_reaction_record, _result, _steps}), - do: {:error, [message: "create reaction fails", code: ecode(:react_fails)]} + defp reaction_result({:error, :create_reaction_record, result, _steps}) do + {:error, [message: "create reaction fails", code: ecode(:react_fails)]} + end defp reaction_result({:error, :add_achievement, _result, _steps}), do: {:error, [message: "achieve fails", code: ecode(:react_fails)]} @@ -74,8 +75,9 @@ defmodule MastaniServer.CMS.Delegate.ArticleReaction do defp undo_reaction_result({:ok, %{delete_reaction_record: result}}), do: result |> done() - defp undo_reaction_result({:error, :delete_reaction_record, _result, _steps}), - do: {:error, [message: "delete reaction fails", code: ecode(:react_fails)]} + defp undo_reaction_result({:error, :delete_reaction_record, result, _steps}) do + {:error, [message: "delete reaction fails", code: ecode(:react_fails)]} + end defp undo_reaction_result({:error, :minus_achievement, _result, _steps}), do: {:error, [message: "achieve fails", code: ecode(:react_fails)]} diff --git a/lib/mastani_server/cms/job.ex b/lib/mastani_server/cms/job.ex index 5cf259341..c8d21d8e1 100644 --- a/lib/mastani_server/cms/job.ex +++ b/lib/mastani_server/cms/job.ex @@ -4,7 +4,16 @@ defmodule MastaniServer.CMS.Job do use Ecto.Schema import Ecto.Changeset - alias MastaniServer.CMS.{Author, Community, JobComment, JobFavorite, JobCommunityFlag, Tag} + + alias MastaniServer.CMS.{ + Author, + Community, + JobComment, + JobFavorite, + JobStar, + JobCommunityFlag, + Tag + } @required_fields ~w(title company company_logo location body digest length)a @optional_fields ~w(link_addr link_source min_education)a @@ -43,7 +52,7 @@ defmodule MastaniServer.CMS.Job do has_many(:comments, {"jobs_comments", JobComment}) has_many(:favorites, {"jobs_favorites", JobFavorite}) - # has_many(:stars, {"posts_stars", PostStar}) + has_many(:stars, {"jobs_stars", JobStar}) many_to_many( :tags, diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index 1114404e4..99e9db9a2 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -13,13 +13,18 @@ defmodule MastaniServer.CMS.Utils.Loader do CommunitySubscriber, CommunityThread, JobCommentReply, + # POST Post, PostComment, PostCommentDislike, PostCommentLike, PostCommentReply, PostFavorite, - PostStar + PostStar, + # JOB + Job, + JobFavorite, + JobStar # job comment # JobComment, } @@ -212,13 +217,15 @@ defmodule MastaniServer.CMS.Utils.Loader do 2. count of the reactions 3. check is viewer reacted """ - def query({"posts_favorites", PostFavorite}, args) do - PostFavorite |> QueryBuilder.members_pack(args) - end + def query({"posts_favorites", PostFavorite}, args), + do: PostFavorite |> QueryBuilder.members_pack(args) - def query({"posts_stars", PostStar}, args) do - PostStar |> QueryBuilder.members_pack(args) - end + def query({"posts_stars", PostStar}, args), do: PostStar |> QueryBuilder.members_pack(args) + + def query({"jobs_favorites", JobFavorite}, args), + do: JobFavorite |> QueryBuilder.members_pack(args) + + def query({"jobs_stars", JobStar}, args), do: JobStar |> QueryBuilder.members_pack(args) def query({"communities_subscribers", CommunitySubscriber}, args) do CommunitySubscriber |> QueryBuilder.members_pack(args) diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 26dfa9f34..657e0b8d6 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -38,6 +38,7 @@ defmodule MastaniServerWeb.Resolvers.CMS do def job(_root, %{id: id}, _info), do: Job |> ORM.read(id, inc: :views) def wiki(_root, ~m(community)a, _info), do: CMS.get_wiki(%Community{raw: community}) + def cheatsheet(_root, ~m(community)a, _info), do: CMS.get_cheatsheet(%Community{raw: community}) def paged_posts(_root, ~m(filter)a, _info), do: Post |> CMS.paged_contents(filter) def paged_videos(_root, ~m(filter)a, _info), do: Video |> CMS.paged_contents(filter) @@ -80,11 +81,11 @@ defmodule MastaniServerWeb.Resolvers.CMS do # thread reaction .. # ####################### def reaction(_root, ~m(id thread action)a, %{context: %{cur_user: user}}) do - CMS.reaction(thread, action, id, user) + CMS.reaction(thread, action, id, user) |> IO.inspect(label: "reaction") end def undo_reaction(_root, ~m(id thread action)a, %{context: %{cur_user: user}}) do - CMS.undo_reaction(thread, action, id, user) + CMS.undo_reaction(thread, action, id, user) |> IO.inspect(label: "undo reaction") end def reaction_users(_root, ~m(id action thread filter)a, _info) do diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index 2acb900a4..456905ee5 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -191,10 +191,13 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do """ interface :article do field(:id, :id) - field(:title, :string) + # field(:title, :string) resolve_type(fn %CMS.Post{}, _ -> :post + %CMS.Job{}, _ -> :job + %CMS.Video{}, _ -> :video + %CMS.Repo{}, _ -> :repo _, _ -> nil end) end diff --git a/lib/mastani_server_web/schema/cms/cms_queries.ex b/lib/mastani_server_web/schema/cms/cms_queries.ex index d31f0f96d..0282db979 100644 --- a/lib/mastani_server_web/schema/cms/cms_queries.ex +++ b/lib/mastani_server_web/schema/cms/cms_queries.ex @@ -112,6 +112,12 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do resolve(&R.CMS.wiki/3) end + @desc "get cheatsheet by community raw name" + field :cheatsheet, non_null(:cheatsheet) do + arg(:community, :string) + resolve(&R.CMS.cheatsheet/3) + end + @desc "get job by id" field :job, non_null(:job) do arg(:id, non_null(:id)) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 5950e2b8d..1f6a69429 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -90,8 +90,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do middleware(M.ConvertToInt) end - field(:inserted_at, :datetime) - field(:updated_at, :datetime) + timestamp_fields() end object :post do @@ -107,8 +106,6 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:pin, :boolean) field(:trash, :boolean) field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) - field(:inserted_at, :datetime) - field(:updated_at, :datetime) field(:author, :user, resolve: dataloader(CMS, :author)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) @@ -171,54 +168,10 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # middleware(M.CountLength) end - field :viewer_has_favorited, :boolean do - arg(:viewer_did, :viewer_did_type, default_value: :viewer_did) - - middleware(M.Authorize, :login) - # put current user into dataloader's args - middleware(M.PutCurrentUser) - resolve(dataloader(CMS, :favorites)) - middleware(M.ViewerDidConvert) - end - - field :viewer_has_starred, :boolean do - arg(:viewer_did, :viewer_did_type, default_value: :viewer_did) - - middleware(M.Authorize, :login) - middleware(M.PutCurrentUser) - resolve(dataloader(CMS, :stars)) - middleware(M.ViewerDidConvert) - end - - field :favorited_users, list_of(:user) do - arg(:filter, :members_filter) - - middleware(M.PageSizeProof) - resolve(dataloader(CMS, :favorites)) - end + favorite_fields(:post) + star_fields(:post) - field :favorited_count, :integer do - arg(:count, :count_type, default_value: :count) - arg(:type, :post_thread, default_value: :post) - # middleware(M.SeeMe) - resolve(dataloader(CMS, :favorites)) - middleware(M.ConvertToInt) - end - - field :starred_count, :integer do - arg(:count, :count_type, default_value: :count) - arg(:type, :post_thread, default_value: :post) - - resolve(dataloader(CMS, :stars)) - middleware(M.ConvertToInt) - end - - field :starred_users, list_of(:user) do - arg(:filter, :members_filter) - - middleware(M.PageSizeProof) - resolve(dataloader(CMS, :stars)) - end + timestamp_fields() end object :video do @@ -246,8 +199,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) - field(:inserted_at, :datetime) - field(:updated_at, :datetime) + timestamp_fields() end object :repo_contributor do @@ -295,8 +247,8 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) - field(:inserted_at, :datetime) - field(:updated_at, :datetime) + + timestamp_fields() end object :job do @@ -316,11 +268,14 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:pin, :boolean) field(:trash, :boolean) + # favorite_count, viewer_did .. + favorite_fields(:job) + field(:author, :user, resolve: dataloader(CMS, :author)) field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) - field(:inserted_at, :datetime) - field(:updated_at, :datetime) + + timestamp_fields() end object :github_contributor do @@ -340,8 +295,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:last_sync, :datetime) field(:views, :integer) - field(:inserted_at, :datetime) - field(:updated_at, :datetime) + timestamp_fields() end object :cheatsheet do @@ -352,8 +306,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:last_sync, :datetime) field(:views, :integer) - field(:inserted_at, :datetime) - field(:updated_at, :datetime) + timestamp_fields() end object :thread do @@ -382,8 +335,6 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:desc, :string) field(:raw, :string) field(:logo, :string) - field(:inserted_at, :datetime) - field(:updated_at, :datetime) field(:author, :user, resolve: dataloader(CMS, :author)) field(:threads, list_of(:thread), resolve: dataloader(CMS, :threads)) field(:categories, list_of(:category), resolve: dataloader(CMS, :categories)) @@ -444,6 +395,8 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # TODO add complex here to warning N+1 problem resolve(&R.Statistics.list_contributes_digest/3) end + + timestamp_fields() end object :category do @@ -452,8 +405,8 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:raw, :string) field(:author, :user, resolve: dataloader(CMS, :author)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) - field(:inserted_at, :datetime) - field(:updated_at, :datetime) + + timestamp_fields() end object :tag do @@ -463,8 +416,8 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:thread, :string) field(:author, :user, resolve: dataloader(CMS, :author)) field(:community, :community, resolve: dataloader(CMS, :community)) - field(:inserted_at, :datetime) - field(:updated_at, :datetime) + + timestamp_fields() end object :paged_categories do diff --git a/lib/mastani_server_web/schema/utils/helper.ex b/lib/mastani_server_web/schema/utils/helper.ex index d63cb25a8..154993ca4 100644 --- a/lib/mastani_server_web/schema/utils/helper.ex +++ b/lib/mastani_server_web/schema/utils/helper.ex @@ -6,6 +6,13 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do @page_size get_config(:general, :page_size) # @default_inner_page_size 5 + defmacro timestamp_fields do + quote do + field(:inserted_at, :datetime) + field(:updated_at, :datetime) + end + end + # see: https://github.com/absinthe-graphql/absinthe/issues/363 defmacro pagination_args do quote do @@ -39,4 +46,77 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do field(:huaban, :string) end end + + import Absinthe.Resolution.Helpers, only: [dataloader: 2] + + alias MastaniServer.CMS + alias MastaniServerWeb.Middleware, as: M + + # fields for: favorite count, users, viewer_did_favorite.. + defmacro favorite_fields(thread) do + quote do + field :viewer_has_favorited, :boolean do + arg(:viewer_did, :viewer_did_type, default_value: :viewer_did) + + middleware(M.Authorize, :login) + middleware(M.PutCurrentUser) + resolve(dataloader(CMS, :favorites)) + middleware(M.ViewerDidConvert) + end + + field :favorited_count, :integer do + arg(:count, :count_type, default_value: :count) + + arg( + :type, + unquote(String.to_atom("#{to_string(thread)}_thread")), + default_value: unquote(thread) + ) + + resolve(dataloader(CMS, :favorites)) + middleware(M.ConvertToInt) + end + + field :favorited_users, list_of(:user) do + arg(:filter, :members_filter) + + middleware(M.PageSizeProof) + resolve(dataloader(CMS, :favorites)) + end + end + end + + # fields for: star count, users, viewer_did_starred.. + defmacro star_fields(thread) do + quote do + field :viewer_has_starred, :boolean do + arg(:viewer_did, :viewer_did_type, default_value: :viewer_did) + + middleware(M.Authorize, :login) + middleware(M.PutCurrentUser) + resolve(dataloader(CMS, :stars)) + middleware(M.ViewerDidConvert) + end + + field :starred_count, :integer do + arg(:count, :count_type, default_value: :count) + + arg( + :type, + unquote(String.to_atom("#{to_string(thread)}_thread")), + default_value: unquote(thread) + ) + + resolve(dataloader(CMS, :stars)) + middleware(M.ConvertToInt) + end + + field :starred_users, list_of(:user) do + arg(:filter, :members_filter) + + middleware(M.PageSizeProof) + resolve(dataloader(CMS, :stars)) + end + end + end end diff --git a/test/mastani_server_web/query/cms/cheatsheet_test.exs b/test/mastani_server_web/query/cms/cheatsheet_test.exs new file mode 100644 index 000000000..7982172c8 --- /dev/null +++ b/test/mastani_server_web/query/cms/cheatsheet_test.exs @@ -0,0 +1,55 @@ +defmodule MastaniServer.Test.Query.Cheatsheet do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, community} = db_insert(:community) + + cheatsheet_attrs = mock_attrs(:cheatsheet, %{community_id: community.id}) + {:ok, cheatsheet} = CMS.sync_github_content(community, :cheatsheet, cheatsheet_attrs) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user) + + {:ok, ~m(user_conn guest_conn community cheatsheet)a} + end + + @query """ + query($community: String!) { + cheatsheet(community: $community) { + id + readme + contributors { + avatar + nickname + } + } + } + """ + test "basic graphql query on cheatsheet", ~m(guest_conn community cheatsheet)a do + variables = %{community: community.raw} + results = guest_conn |> query_result(@query, variables, "cheatsheet") + + assert results["id"] == to_string(cheatsheet.id) + assert is_valid_kv?(results, "readme", :string) + assert results["contributors"] |> length !== 0 + end + + @query """ + query($community: String!) { + cheatsheet(community: $community) { + views + } + } + """ + test "views should +1 after query the cheatsheet", ~m(guest_conn community)a do + variables = %{community: community.raw} + views_1 = guest_conn |> query_result(@query, variables, "cheatsheet") |> Map.get("views") + + variables = %{community: community.raw} + views_2 = guest_conn |> query_result(@query, variables, "cheatsheet") |> Map.get("views") + + assert views_2 == views_1 + 1 + end +end From 85d8b67ea05d80958b39f5665dfdda15fcf16903 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 7 Oct 2018 21:04:54 +0800 Subject: [PATCH 040/129] feat(video reactions): add basic favorite / star --- lib/mastani_server/cms/utils/matcher.ex | 8 +++ lib/mastani_server/cms/video.ex | 11 ++-- lib/mastani_server/cms/video_favorite.ex | 27 ++++++++++ lib/mastani_server/cms/video_star.ex | 27 ++++++++++ ...20181007124749_create_videos_favorites.exs | 14 +++++ .../20181007125858_create_videos_stars.exs | 14 +++++ .../cms/video_reactions_test.exs | 51 +++++++++++++++++++ .../mutation/accounts/account_test.exs | 2 - 8 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 lib/mastani_server/cms/video_favorite.ex create mode 100644 lib/mastani_server/cms/video_star.ex create mode 100644 priv/repo/migrations/20181007124749_create_videos_favorites.exs create mode 100644 priv/repo/migrations/20181007125858_create_videos_stars.exs create mode 100644 test/mastani_server/cms/video_reactions_test.exs diff --git a/lib/mastani_server/cms/utils/matcher.ex b/lib/mastani_server/cms/utils/matcher.ex index 5f7598aa4..7d09736cb 100644 --- a/lib/mastani_server/cms/utils/matcher.ex +++ b/lib/mastani_server/cms/utils/matcher.ex @@ -14,8 +14,10 @@ defmodule MastaniServer.CMS.Utils.Matcher do # reactions PostFavorite, JobFavorite, + VideoFavorite, PostStar, JobStar, + VideoStar, # comments PostComment, JobComment, @@ -109,6 +111,12 @@ defmodule MastaniServer.CMS.Utils.Matcher do def match_action(:video_comment, :dislike), do: {:ok, %{target: VideoComment, reactor: VideoCommentDislike}} + def match_action(:video, :favorite), + do: {:ok, %{target: Video, reactor: VideoFavorite, preload: :user}} + + def match_action(:video, :star), + do: {:ok, %{target: Video, reactor: VideoStar, preload: :user}} + ######################################### ## repos ... ######################################### diff --git a/lib/mastani_server/cms/video.ex b/lib/mastani_server/cms/video.ex index 53618da88..d5efa22f0 100644 --- a/lib/mastani_server/cms/video.ex +++ b/lib/mastani_server/cms/video.ex @@ -4,7 +4,7 @@ defmodule MastaniServer.CMS.Video do use Ecto.Schema import Ecto.Changeset - alias MastaniServer.CMS.{Author, Community, VideoCommunityFlag, Tag} + alias MastaniServer.CMS.{Author, Community, VideoFavorite, VideoCommunityFlag, VideoStar, Tag} @required_fields ~w(title poster thumbnil desc duration duration_sec source link original_author original_author_link publish_at)a # @optional_fields ~w()a @@ -17,7 +17,7 @@ defmodule MastaniServer.CMS.Video do field(:desc, :string) field(:duration, :string) field(:duration_sec, :integer) - + belongs_to(:author, Author) field(:source, :string) field(:link, :string) @@ -25,6 +25,7 @@ defmodule MastaniServer.CMS.Video do field(:original_author_link, :string) field(:views, :integer, default: 0) + field(:publish_at, :utc_datetime) has_many(:community_flags, {"videos_communities_flags", VideoCommunityFlag}) @@ -32,10 +33,8 @@ defmodule MastaniServer.CMS.Video do field(:pin, :boolean, default_value: false) field(:trash, :boolean, default_value: false) - field(:publish_at, :utc_datetime) - - belongs_to(:author, Author) - + has_many(:favorites, {"videos_favorites", VideoFavorite}) + has_many(:stars, {"videos_stars", VideoStar}) # has_many(:comments, {"posts_comments", PostComment}) many_to_many( diff --git a/lib/mastani_server/cms/video_favorite.ex b/lib/mastani_server/cms/video_favorite.ex new file mode 100644 index 000000000..ae885883e --- /dev/null +++ b/lib/mastani_server/cms/video_favorite.ex @@ -0,0 +1,27 @@ +defmodule MastaniServer.CMS.VideoFavorite do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.Video + + @required_fields ~w(user_id video_id)a + + @type t :: %VideoFavorite{} + schema "videos_favorites" do + belongs_to(:user, Accounts.User, foreign_key: :user_id) + belongs_to(:video, Video, foreign_key: :video_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%VideoFavorite{} = video_favorite, attrs) do + video_favorite + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> unique_constraint(:user_id, name: :videos_favorites_user_id_video_id_index) + end +end diff --git a/lib/mastani_server/cms/video_star.ex b/lib/mastani_server/cms/video_star.ex new file mode 100644 index 000000000..b788d9d65 --- /dev/null +++ b/lib/mastani_server/cms/video_star.ex @@ -0,0 +1,27 @@ +defmodule MastaniServer.CMS.VideoStar do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.Video + + @required_fields ~w(user_id video_id)a + + @type t :: %VideoStar{} + schema "videos_stars" do + belongs_to(:user, Accounts.User, foreign_key: :user_id) + belongs_to(:video, Video, foreign_key: :video_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%VideoStar{} = video_star, attrs) do + video_star + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> unique_constraint(:user_id, name: :videos_stars_user_id_video_id_index) + end +end diff --git a/priv/repo/migrations/20181007124749_create_videos_favorites.exs b/priv/repo/migrations/20181007124749_create_videos_favorites.exs new file mode 100644 index 000000000..b5136f353 --- /dev/null +++ b/priv/repo/migrations/20181007124749_create_videos_favorites.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreateVideosFavorites do + use Ecto.Migration + + def change do + create table(:videos_favorites) do + add(:user_id, references(:users, on_delete: :delete_all), null: false) + add(:video_id, references(:cms_videos, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:videos_favorites, [:user_id, :video_id])) + end +end diff --git a/priv/repo/migrations/20181007125858_create_videos_stars.exs b/priv/repo/migrations/20181007125858_create_videos_stars.exs new file mode 100644 index 000000000..318f2e4ea --- /dev/null +++ b/priv/repo/migrations/20181007125858_create_videos_stars.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreateVideosStars do + use Ecto.Migration + + def change do + create table(:videos_stars) do + add(:user_id, references(:users, on_delete: :delete_all), null: false) + add(:video_id, references(:cms_videos, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:videos_stars, [:user_id, :video_id])) + end +end diff --git a/test/mastani_server/cms/video_reactions_test.exs b/test/mastani_server/cms/video_reactions_test.exs new file mode 100644 index 000000000..f2cb3d345 --- /dev/null +++ b/test/mastani_server/cms/video_reactions_test.exs @@ -0,0 +1,51 @@ +defmodule MastaniServer.Test.VideoReactions do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, user} = db_insert(:user) + {:ok, community} = db_insert(:community) + + video_attrs = mock_attrs(:video, %{community_id: community.id}) + + {:ok, ~m(user community video_attrs)a} + end + + describe "[cms video star/favorite reaction]" do + @tag :wip + test "star and undo star reaction to video", ~m(user community video_attrs)a do + {:ok, video} = CMS.create_content(community, :video, video_attrs, user) + + {:ok, _} = CMS.reaction(:video, :star, video.id, user) + {:ok, reaction_users} = CMS.reaction_users(:video, :star, video.id, %{page: 1, size: 1}) + reaction_users = reaction_users |> Map.get(:entries) + assert 1 == reaction_users |> Enum.filter(fn ruser -> user.id == ruser.id end) |> length + + {:ok, _} = CMS.undo_reaction(:video, :star, video.id, user) + {:ok, reaction_users2} = CMS.reaction_users(:video, :star, video.id, %{page: 1, size: 1}) + reaction_users2 = reaction_users2 |> Map.get(:entries) + + assert 0 == reaction_users2 |> Enum.filter(fn ruser -> user.id == ruser.id end) |> length + end + + @tag :wip + test "favorite and undo favorite reaction to video", ~m(user community video_attrs)a do + {:ok, video} = CMS.create_content(community, :video, video_attrs, user) + + {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) + {:ok, reaction_users} = CMS.reaction_users(:video, :favorite, video.id, %{page: 1, size: 1}) + reaction_users = reaction_users |> Map.get(:entries) + assert 1 == reaction_users |> Enum.filter(fn ruser -> user.id == ruser.id end) |> length + + {:ok, _} = CMS.undo_reaction(:video, :favorite, video.id, user) + + {:ok, reaction_users2} = + CMS.reaction_users(:video, :favorite, video.id, %{page: 1, size: 1}) + + reaction_users2 = reaction_users2 |> Map.get(:entries) + + assert 0 == reaction_users2 |> Enum.filter(fn ruser -> user.id == ruser.id end) |> length + end + end +end diff --git a/test/mastani_server_web/mutation/accounts/account_test.exs b/test/mastani_server_web/mutation/accounts/account_test.exs index 5a3ad4a65..370857d30 100644 --- a/test/mastani_server_web/mutation/accounts/account_test.exs +++ b/test/mastani_server_web/mutation/accounts/account_test.exs @@ -91,7 +91,6 @@ defmodule MastaniServer.Test.Mutation.Account.Basic do assert updated["work_backgrounds"] |> Enum.any?(&(&1["title"] == "CTO")) end - @tag :wip test "user update education_backgrounds with invalid data fails", ~m(user)a do ownd_conn = simu_conn(:user, user) @@ -113,7 +112,6 @@ defmodule MastaniServer.Test.Mutation.Account.Basic do assert ownd_conn |> mutation_get_error?(@update_query, variables) end - @tag :wip test "user update work backgrounds with invalid data fails", ~m(user)a do ownd_conn = simu_conn(:user, user) From a344032dc054111532421180f43f130f8e2081ce Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 7 Oct 2018 21:59:01 +0800 Subject: [PATCH 041/129] chore(reactions): add reactions test --- lib/helper/query_builder.ex | 6 + lib/mastani_server/cms/utils/loader.ex | 29 +++-- .../resolvers/cms_resolver.ex | 4 +- .../schema/cms/cms_types.ex | 8 +- .../mutation/cms/job_reaction_test.exs | 115 ++++++++++++++++++ .../mutation/cms/post_reaction_test.exs | 115 ++++++++++++++++++ .../mutation/cms/video_reaction_test.exs | 115 ++++++++++++++++++ 7 files changed, 379 insertions(+), 13 deletions(-) create mode 100644 test/mastani_server_web/mutation/cms/job_reaction_test.exs create mode 100644 test/mastani_server_web/mutation/cms/post_reaction_test.exs create mode 100644 test/mastani_server_web/mutation/cms/video_reaction_test.exs diff --git a/lib/helper/query_builder.ex b/lib/helper/query_builder.ex index 95979b8c5..6acf8d6a1 100644 --- a/lib/helper/query_builder.ex +++ b/lib/helper/query_builder.ex @@ -33,6 +33,12 @@ defmodule Helper.QueryBuilder do |> select([f], count(f.id)) end + def members_pack(queryable, %{count: _, type: :video}) do + queryable + |> group_by([f], f.job_id) + |> select([f], count(f.id)) + end + def members_pack(queryable, %{count: _, type: :community}) do queryable |> group_by([f], f.community_id) diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index 99e9db9a2..92d40ab96 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -24,9 +24,12 @@ defmodule MastaniServer.CMS.Utils.Loader do # JOB Job, JobFavorite, - JobStar + JobStar, # job comment # JobComment, + # Video + VideoFavorite, + VideoStar } def data, do: Dataloader.Ecto.new(Repo, query: &query/2, run_batch: &run_batch/5) @@ -217,15 +220,27 @@ defmodule MastaniServer.CMS.Utils.Loader do 2. count of the reactions 3. check is viewer reacted """ - def query({"posts_favorites", PostFavorite}, args), - do: PostFavorite |> QueryBuilder.members_pack(args) + def query({"posts_favorites", PostFavorite}, args) do + PostFavorite |> QueryBuilder.members_pack(args) + end + + def query({"posts_stars", PostStar}, args) do + PostStar |> QueryBuilder.members_pack(args) + end - def query({"posts_stars", PostStar}, args), do: PostStar |> QueryBuilder.members_pack(args) + def query({"jobs_favorites", JobFavorite}, args) do + JobFavorite |> QueryBuilder.members_pack(args) + end - def query({"jobs_favorites", JobFavorite}, args), - do: JobFavorite |> QueryBuilder.members_pack(args) + # def query({"jobs_stars", JobStar}, args), do: JobStar |> QueryBuilder.members_pack(args) - def query({"jobs_stars", JobStar}, args), do: JobStar |> QueryBuilder.members_pack(args) + def query({"videos_favorites", VideoFavorite}, args) do + VideoFavorite |> QueryBuilder.members_pack(args) + end + + def query({"videos_stars", VideoStar}, args) do + VideoStar |> QueryBuilder.members_pack(args) + end def query({"communities_subscribers", CommunitySubscriber}, args) do CommunitySubscriber |> QueryBuilder.members_pack(args) diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 657e0b8d6..3d87ab418 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -81,11 +81,11 @@ defmodule MastaniServerWeb.Resolvers.CMS do # thread reaction .. # ####################### def reaction(_root, ~m(id thread action)a, %{context: %{cur_user: user}}) do - CMS.reaction(thread, action, id, user) |> IO.inspect(label: "reaction") + CMS.reaction(thread, action, id, user) end def undo_reaction(_root, ~m(id thread action)a, %{context: %{cur_user: user}}) do - CMS.undo_reaction(thread, action, id, user) |> IO.inspect(label: "undo reaction") + CMS.undo_reaction(thread, action, id, user) end def reaction_users(_root, ~m(id action thread filter)a, _info) do diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 1f6a69429..6541e791f 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -193,12 +193,12 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:pin, :boolean) field(:trash, :boolean) - # TODO: remove - # field(:pin, :boolean) - # field(:trash, :boolean) # field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + + favorite_fields(:video) + star_fields(:video) timestamp_fields() end @@ -269,12 +269,12 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:trash, :boolean) # favorite_count, viewer_did .. - favorite_fields(:job) field(:author, :user, resolve: dataloader(CMS, :author)) field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + favorite_fields(:job) timestamp_fields() end diff --git a/test/mastani_server_web/mutation/cms/job_reaction_test.exs b/test/mastani_server_web/mutation/cms/job_reaction_test.exs new file mode 100644 index 000000000..8fa2e3600 --- /dev/null +++ b/test/mastani_server_web/mutation/cms/job_reaction_test.exs @@ -0,0 +1,115 @@ +defmodule MastaniServer.Test.Mutation.JobReaction do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, job} = db_insert(:job) + {:ok, user} = db_insert(:user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(user_conn guest_conn job user)a} + end + + describe "[job favorite]" do + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + reaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can favorite a job", ~m(user_conn job)a do + variables = %{id: job.id, thread: "JOB", action: "FAVORITE"} + created = user_conn |> mutation_result(@query, variables, "reaction") + + assert created["id"] == to_string(job.id) + end + + @tag :wip + test "unauth user favorite a job fails", ~m(guest_conn job)a do + variables = %{id: job.id, thread: "JOB", action: "FAVORITE"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + undoReaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can undo favorite a job", ~m(user_conn job user)a do + {:ok, _} = CMS.reaction(:job, :favorite, job.id, user) + + variables = %{id: job.id, thread: "JOB", action: "FAVORITE"} + updated = user_conn |> mutation_result(@query, variables, "undoReaction") + + assert updated["id"] == to_string(job.id) + end + + @tag :wip + test "unauth user undo favorite a job fails", ~m(guest_conn job)a do + variables = %{id: job.id, thread: "JOB", action: "FAVORITE"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + end + + describe "[job star]" do + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + reaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can star a job", ~m(user_conn job)a do + variables = %{id: job.id, thread: "JOB", action: "STAR"} + created = user_conn |> mutation_result(@query, variables, "reaction") + + assert created["id"] == to_string(job.id) + end + + @tag :wip + test "unauth user star a job fails", ~m(guest_conn job)a do + variables = %{id: job.id, thread: "JOB", action: "STAR"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + undoReaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can undo star a job", ~m(user_conn job user)a do + {:ok, _} = CMS.reaction(:job, :star, job.id, user) + + variables = %{id: job.id, thread: "JOB", action: "STAR"} + updated = user_conn |> mutation_result(@query, variables, "undoReaction") + + assert updated["id"] == to_string(job.id) + end + + @tag :wip + test "unauth user undo star a job fails", ~m(guest_conn job)a do + variables = %{id: job.id, thread: "JOB", action: "STAR"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + end +end diff --git a/test/mastani_server_web/mutation/cms/post_reaction_test.exs b/test/mastani_server_web/mutation/cms/post_reaction_test.exs new file mode 100644 index 000000000..a7943ab2c --- /dev/null +++ b/test/mastani_server_web/mutation/cms/post_reaction_test.exs @@ -0,0 +1,115 @@ +defmodule MastaniServer.Test.Mutation.PostReaction do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, post} = db_insert(:post) + {:ok, user} = db_insert(:user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(user_conn guest_conn post user)a} + end + + describe "[post favorite]" do + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + reaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can favorite a post", ~m(user_conn post)a do + variables = %{id: post.id, thread: "POST", action: "FAVORITE"} + created = user_conn |> mutation_result(@query, variables, "reaction") + + assert created["id"] == to_string(post.id) + end + + @tag :wip + test "unauth user favorite a post fails", ~m(guest_conn post)a do + variables = %{id: post.id, thread: "POST", action: "FAVORITE"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + undoReaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can undo favorite a post", ~m(user_conn post user)a do + {:ok, _} = CMS.reaction(:post, :favorite, post.id, user) + + variables = %{id: post.id, thread: "POST", action: "FAVORITE"} + updated = user_conn |> mutation_result(@query, variables, "undoReaction") + + assert updated["id"] == to_string(post.id) + end + + @tag :wip + test "unauth user undo favorite a post fails", ~m(guest_conn post)a do + variables = %{id: post.id, thread: "POST", action: "FAVORITE"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + end + + describe "[post star]" do + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + reaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can star a post", ~m(user_conn post)a do + variables = %{id: post.id, thread: "POST", action: "STAR"} + created = user_conn |> mutation_result(@query, variables, "reaction") + + assert created["id"] == to_string(post.id) + end + + @tag :wip + test "unauth user star a post fails", ~m(guest_conn post)a do + variables = %{id: post.id, thread: "POST", action: "STAR"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + undoReaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can undo star a post", ~m(user_conn post user)a do + {:ok, _} = CMS.reaction(:post, :star, post.id, user) + + variables = %{id: post.id, thread: "POST", action: "STAR"} + updated = user_conn |> mutation_result(@query, variables, "undoReaction") + + assert updated["id"] == to_string(post.id) + end + + @tag :wip + test "unauth user undo star a post fails", ~m(guest_conn post)a do + variables = %{id: post.id, thread: "POST", action: "STAR"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + end +end diff --git a/test/mastani_server_web/mutation/cms/video_reaction_test.exs b/test/mastani_server_web/mutation/cms/video_reaction_test.exs new file mode 100644 index 000000000..b4180f239 --- /dev/null +++ b/test/mastani_server_web/mutation/cms/video_reaction_test.exs @@ -0,0 +1,115 @@ +defmodule MastaniServer.Test.Mutation.VideoReaction do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, video} = db_insert(:video) + {:ok, user} = db_insert(:user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(user_conn guest_conn video user)a} + end + + describe "[video favorite]" do + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + reaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can favorite a video", ~m(user_conn video)a do + variables = %{id: video.id, thread: "VIDEO", action: "FAVORITE"} + created = user_conn |> mutation_result(@query, variables, "reaction") + + assert created["id"] == to_string(video.id) + end + + @tag :wip + test "unauth user favorite a video fails", ~m(guest_conn video)a do + variables = %{id: video.id, thread: "VIDEO", action: "FAVORITE"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + undoReaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can undo favorite a video", ~m(user_conn video user)a do + {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) + + variables = %{id: video.id, thread: "VIDEO", action: "FAVORITE"} + updated = user_conn |> mutation_result(@query, variables, "undoReaction") + + assert updated["id"] == to_string(video.id) + end + + @tag :wip + test "unauth user undo favorite a video fails", ~m(guest_conn video)a do + variables = %{id: video.id, thread: "VIDEO", action: "FAVORITE"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + end + + describe "[video star]" do + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + reaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can star a video", ~m(user_conn video)a do + variables = %{id: video.id, thread: "VIDEO", action: "STAR"} + created = user_conn |> mutation_result(@query, variables, "reaction") + + assert created["id"] == to_string(video.id) + end + + @tag :wip + test "unauth user star a video fails", ~m(guest_conn video)a do + variables = %{id: video.id, thread: "VIDEO", action: "STAR"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + undoReaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can undo star a video", ~m(user_conn video user)a do + {:ok, _} = CMS.reaction(:video, :star, video.id, user) + + variables = %{id: video.id, thread: "VIDEO", action: "STAR"} + updated = user_conn |> mutation_result(@query, variables, "undoReaction") + + assert updated["id"] == to_string(video.id) + end + + @tag :wip + test "unauth user undo star a video fails", ~m(guest_conn video)a do + variables = %{id: video.id, thread: "VIDEO", action: "STAR"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + end +end From fb5847b9249af6a38d305d1485a62a9d493892d5 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 7 Oct 2018 22:52:09 +0800 Subject: [PATCH 042/129] fix(video reaction): job_id -> video_id --- lib/helper/query_builder.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helper/query_builder.ex b/lib/helper/query_builder.ex index 6acf8d6a1..b8c0a1d34 100644 --- a/lib/helper/query_builder.ex +++ b/lib/helper/query_builder.ex @@ -35,7 +35,7 @@ defmodule Helper.QueryBuilder do def members_pack(queryable, %{count: _, type: :video}) do queryable - |> group_by([f], f.job_id) + |> group_by([f], f.video_id) |> select([f], count(f.id)) end From 585081263c0c8a231fc7ce3f31f7c31319b92b5e Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 8 Oct 2018 12:44:14 +0800 Subject: [PATCH 043/129] test(video comments): add like/dislike tests --- lib/mastani_server/cms/utils/loader.ex | 61 ++++- lib/mastani_server/cms/video.ex | 13 +- .../resolvers/cms_resolver.ex | 5 +- lib/mastani_server_web/schema/cms/cms_misc.ex | 8 +- .../query/cms/video_comment_test.exs | 213 ++++++++++++++++++ 5 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 test/mastani_server_web/query/cms/video_comment_test.exs diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index 92d40ab96..ded885872 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -12,7 +12,6 @@ defmodule MastaniServer.CMS.Utils.Loader do CommunityEditor, CommunitySubscriber, CommunityThread, - JobCommentReply, # POST Post, PostComment, @@ -25,11 +24,15 @@ defmodule MastaniServer.CMS.Utils.Loader do Job, JobFavorite, JobStar, + JobCommentReply, # job comment # JobComment, # Video VideoFavorite, - VideoStar + VideoStar, + VideoCommentReply, + VideoCommentDislike, + VideoCommentLike } def data, do: Dataloader.Ecto.new(Repo, query: &query/2, run_batch: &run_batch/5) @@ -319,6 +322,60 @@ defmodule MastaniServer.CMS.Utils.Loader do # ---- job ------ + # ---- video comments ------ + def query({"videos_comments_replies", VideoCommentReply}, %{count: _}) do + VideoCommentReply + |> group_by([c], c.video_comment_id) + |> select([c], count(c.id)) + end + + def query({"videos_comments_replies", VideoCommentReply}, %{filter: filter}) do + VideoCommentReply + |> QueryBuilder.load_inner_replies(filter) + end + + def query({"videos_comments_replies", VideoCommentReply}, %{reply_to: _}) do + VideoCommentReply + |> join(:inner, [c], r in assoc(c, :video_comment)) + |> select([c, r], r) + end + + def query({"videos_comments_likes", VideoCommentLike}, %{count: _}) do + VideoCommentLike + |> group_by([f], f.video_comment_id) + |> select([f], count(f.id)) + end + + def query({"videos_comments_likes", VideoCommentLike}, %{viewer_did: _, cur_user: cur_user}) do + VideoCommentLike |> where([f], f.user_id == ^cur_user.id) + end + + def query({"videos_comments_likes", VideoCommentLike}, %{filter: _filter} = args) do + VideoCommentLike + |> QueryBuilder.members_pack(args) + end + + def query({"videos_comments_dislikes", VideoCommentDislike}, %{count: _}) do + VideoCommentDislike + |> group_by([f], f.video_comment_id) + |> select([f], count(f.id)) + end + + # component dislikes + def query({"videos_comments_dislikes", VideoCommentDislike}, %{ + viewer_did: _, + cur_user: cur_user + }) do + VideoCommentDislike |> where([f], f.user_id == ^cur_user.id) + end + + def query({"videos_comments_dislikes", VideoCommentDislike}, %{filter: _filter} = args) do + VideoCommentDislike + |> QueryBuilder.members_pack(args) + end + + # ---- video ------ + # default loader def query(queryable, _args) do # IO.inspect(queryable, label: "default loader") diff --git a/lib/mastani_server/cms/video.ex b/lib/mastani_server/cms/video.ex index d5efa22f0..6d6b920c9 100644 --- a/lib/mastani_server/cms/video.ex +++ b/lib/mastani_server/cms/video.ex @@ -4,7 +4,16 @@ defmodule MastaniServer.CMS.Video do use Ecto.Schema import Ecto.Changeset - alias MastaniServer.CMS.{Author, Community, VideoFavorite, VideoCommunityFlag, VideoStar, Tag} + + alias MastaniServer.CMS.{ + Author, + Community, + VideoComment, + VideoFavorite, + VideoCommunityFlag, + VideoStar, + Tag + } @required_fields ~w(title poster thumbnil desc duration duration_sec source link original_author original_author_link publish_at)a # @optional_fields ~w()a @@ -35,7 +44,7 @@ defmodule MastaniServer.CMS.Video do has_many(:favorites, {"videos_favorites", VideoFavorite}) has_many(:stars, {"videos_stars", VideoStar}) - # has_many(:comments, {"posts_comments", PostComment}) + has_many(:comments, {"videos_comments", VideoComment}) many_to_many( :tags, diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 3d87ab418..7756f6913 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -215,8 +215,9 @@ defmodule MastaniServerWeb.Resolvers.CMS do # ####################### # comemnts .. # ####################### - def paged_comments(_root, ~m(id thread filter)a, _info), - do: CMS.list_comments(thread, id, filter) + def paged_comments(_root, ~m(id thread filter)a, _info) do + CMS.list_comments(thread, id, filter) + end def create_comment(_root, ~m(thread id body)a, %{context: %{cur_user: user}}) do CMS.create_comment(thread, id, body, user) diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index 456905ee5..1e9ba94b5 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -19,7 +19,6 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do enum(:viewer_did_type, do: value(:viewer_did)) enum(:favorite_action, do: value(:favorite)) - enum(:cms_comment, do: value(:post_comment)) enum(:star_action, do: value(:star)) enum(:comment_action, do: value(:comment)) @@ -34,6 +33,13 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do value(:watch) end + enum :cms_comment do + value(:post_comment) + value(:job_comment) + value(:video_comment) + value(:repo_comment) + end + enum :cms_thread do value(:post) value(:job) diff --git a/test/mastani_server_web/query/cms/video_comment_test.exs b/test/mastani_server_web/query/cms/video_comment_test.exs new file mode 100644 index 000000000..5fc1e36d6 --- /dev/null +++ b/test/mastani_server_web/query/cms/video_comment_test.exs @@ -0,0 +1,213 @@ +defmodule MastaniServer.Test.Query.VideoComment do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, video} = db_insert(:video) + {:ok, user} = db_insert(:user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user) + + {:ok, ~m(user_conn guest_conn video user)a} + end + + # TODO: user can get specific user's replies :list_replies + describe "[video comment]" do + @query """ + query($thread: CmsThread, $id: ID!, $filter: CommentsFilter!) { + pagedComments(thread: $thread, id: $id, filter: $filter) { + entries { + id + body + likesCount + dislikesCount + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + @tag :wip + test "guest user can get a paged comment", ~m(guest_conn video user)a do + body = "test comment" + + Enum.reduce(1..30, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:video, video.id, body, user) + + acc ++ [value] + end) + + variables = %{thread: "VIDEO", id: video.id, filter: %{page: 1, size: 10}} + results = guest_conn |> query_result(@query, variables, "pagedComments") + + assert results |> is_valid_pagination? + assert results["totalCount"] == 30 + end + + @tag :wip2 + test "MOST_LIKES filter should work", ~m(guest_conn video user)a do + body = "test comment" + + comments = + Enum.reduce(1..10, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:video, video.id, body, user) + + acc ++ [value] + end) + + [comment_1, _comment_2, comment_3, _comment_last] = comments |> firstn_and_last(3) + + {:ok, [user_1, user_2, user_3, user_4, user_5]} = db_insert_multi(:user, 5) + + # comment_3 is most likes + {:ok, _} = CMS.like_comment(:video_comment, comment_3.id, user_1) + {:ok, _} = CMS.like_comment(:video_comment, comment_3.id, user_2) + {:ok, _} = CMS.like_comment(:video_comment, comment_3.id, user_3) + {:ok, _} = CMS.like_comment(:video_comment, comment_3.id, user_4) + {:ok, _} = CMS.like_comment(:video_comment, comment_3.id, user_5) + + {:ok, _} = CMS.like_comment(:video_comment, comment_1.id, user_1) + {:ok, _} = CMS.like_comment(:video_comment, comment_1.id, user_2) + {:ok, _} = CMS.like_comment(:video_comment, comment_1.id, user_3) + {:ok, _} = CMS.like_comment(:video_comment, comment_1.id, user_4) + + variables = %{ + thread: "VIDEO", + id: video.id, + filter: %{page: 1, size: 10, sort: "MOST_LIKES"} + } + + results = guest_conn |> query_result(@query, variables, "pagedComments") + + entries = results["entries"] + + assert entries |> Enum.at(0) |> Map.get("id") == to_string(comment_3.id) + assert entries |> Enum.at(0) |> Map.get("likesCount") == 5 + + assert entries |> Enum.at(1) |> Map.get("id") == to_string(comment_1.id) + assert entries |> Enum.at(1) |> Map.get("likesCount") == 4 + end + + @tag :wip + test "MOST_DISLIKES filter should work", ~m(guest_conn video user)a do + body = "test comment" + + comments = + Enum.reduce(1..10, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:video, video.id, body, user) + + acc ++ [value] + end) + + [comment_1, _comment_2, comment_3, _comment_last] = comments |> firstn_and_last(3) + {:ok, [user_1, user_2, user_3, user_4, user_5]} = db_insert_multi(:user, 5) + + # comment_3 is most likes + {:ok, _} = CMS.dislike_comment(:video_comment, comment_3.id, user_1) + {:ok, _} = CMS.dislike_comment(:video_comment, comment_3.id, user_2) + {:ok, _} = CMS.dislike_comment(:video_comment, comment_3.id, user_3) + {:ok, _} = CMS.dislike_comment(:video_comment, comment_3.id, user_4) + {:ok, _} = CMS.dislike_comment(:video_comment, comment_3.id, user_5) + + {:ok, _} = CMS.dislike_comment(:video_comment, comment_1.id, user_1) + {:ok, _} = CMS.dislike_comment(:video_comment, comment_1.id, user_2) + {:ok, _} = CMS.dislike_comment(:video_comment, comment_1.id, user_3) + {:ok, _} = CMS.dislike_comment(:video_comment, comment_1.id, user_4) + + variables = %{ + thread: "VIDEO", + id: video.id, + filter: %{page: 1, size: 10, sort: "MOST_DISLIKES"} + } + + results = guest_conn |> query_result(@query, variables, "pagedComments") + entries = results["entries"] + + assert entries |> Enum.at(0) |> Map.get("id") == to_string(comment_3.id) + assert entries |> Enum.at(0) |> Map.get("dislikesCount") == 5 + + assert entries |> Enum.at(1) |> Map.get("id") == to_string(comment_1.id) + assert entries |> Enum.at(1) |> Map.get("dislikesCount") == 4 + end + + @query """ + query($thread: CmsThread, $id: ID!, $filter: CommentsFilter!) { + pagedComments(thread: $thread, id: $id, filter: $filter) { + entries { + id + viewerHasLiked + } + } + } + """ + @tag :wip + test "login user can get hasLiked feedBack", ~m(user_conn video user)a do + body = "test comment" + + {:ok, comment} = CMS.create_comment(:video, video.id, body, user) + + {:ok, _like} = CMS.like_comment(:video_comment, comment.id, user) + + variables = %{thread: "VIDEO", id: video.id, filter: %{page: 1, size: 10}} + results = user_conn |> query_result(@query, variables, "pagedComments") + + found = + results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() + + assert found["viewerHasLiked"] == false + + own_like_conn = simu_conn(:user, user) + results = own_like_conn |> query_result(@query, variables, "pagedComments") + + found = + results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() + + assert found["viewerHasLiked"] == true + end + + @query """ + query($thread: CmsThread, $id: ID!, $filter: PagedFilter!) { + pagedComments(thread: $thread, id: $id, filter: $filter) { + entries { + id + body + replyTo { + id + body + } + repliesCount + replies { + id + body + } + } + } + } + """ + @tag :wip + test "guest user can get replies info", ~m(guest_conn video user)a do + body = "test comment" + {:ok, comment} = CMS.create_comment(:video, video.id, body, user) + + {:ok, reply} = CMS.reply_comment(:video, comment.id, "reply body", user) + + variables = %{thread: "VIDEO", id: video.id, filter: %{page: 1, size: 10}} + results = guest_conn |> query_result(@query, variables, "pagedComments") + + found = + results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() + + found_reply = + results["entries"] |> Enum.filter(&(&1["id"] == to_string(reply.id))) |> List.first() + + assert found["repliesCount"] == 1 + assert found["replies"] |> Enum.any?(&(&1["id"] == to_string(reply.id))) + assert found["replyTo"] == nil + assert found_reply["replyTo"] |> Map.get("id") == to_string(comment.id) + end + end +end From 8c51ac790f7b446c699a9bf16f046345bfe3ce1e Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 8 Oct 2018 13:10:11 +0800 Subject: [PATCH 044/129] refactor(rename): comments -> pagedComments --- .../schema/cms/cms_queries.ex | 1 + .../query/cms/job_comment_test.exs | 8 +++--- .../query/cms/post_comment_test.exs | 26 +++++++++---------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/mastani_server_web/schema/cms/cms_queries.ex b/lib/mastani_server_web/schema/cms/cms_queries.ex index 0282db979..b7a7f4254 100644 --- a/lib/mastani_server_web/schema/cms/cms_queries.ex +++ b/lib/mastani_server_web/schema/cms/cms_queries.ex @@ -180,6 +180,7 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do end # comments + # TODO: remove field :comments, :paged_comments do arg(:id, non_null(:id)) arg(:thread, :cms_thread, default_value: :post) diff --git a/test/mastani_server_web/query/cms/job_comment_test.exs b/test/mastani_server_web/query/cms/job_comment_test.exs index f0a8777e8..97583afb4 100644 --- a/test/mastani_server_web/query/cms/job_comment_test.exs +++ b/test/mastani_server_web/query/cms/job_comment_test.exs @@ -17,7 +17,7 @@ defmodule MastaniServer.Test.Query.JobComment do describe "[job comment]" do @query """ query($thread: CmsThread, $id: ID!, $filter: CommentsFilter!) { - comments(thread: $thread, id: $id, filter: $filter) { + pagedComments(thread: $thread, id: $id, filter: $filter) { entries { id body @@ -39,7 +39,7 @@ defmodule MastaniServer.Test.Query.JobComment do end) variables = %{thread: "JOB", id: job.id, filter: %{page: 1, size: 10}} - results = guest_conn |> query_result(@query, variables, "comments") + results = guest_conn |> query_result(@query, variables, "pagedComments") assert results |> is_valid_pagination? assert results["totalCount"] == 30 @@ -47,7 +47,7 @@ defmodule MastaniServer.Test.Query.JobComment do @query """ query($thread: CmsThread, $id: ID!, $filter: PagedFilter!) { - comments(thread: $thread, id: $id, filter: $filter) { + pagedComments(thread: $thread, id: $id, filter: $filter) { entries { id body @@ -72,7 +72,7 @@ defmodule MastaniServer.Test.Query.JobComment do {:ok, reply} = CMS.reply_comment(:job, comment.id, "reply body", user) variables = %{thread: "JOB", id: job.id, filter: %{page: 1, size: 10}} - results = guest_conn |> query_result(@query, variables, "comments") + results = guest_conn |> query_result(@query, variables, "pagedComments") found = results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() diff --git a/test/mastani_server_web/query/cms/post_comment_test.exs b/test/mastani_server_web/query/cms/post_comment_test.exs index 0f8b3273c..804f57c84 100644 --- a/test/mastani_server_web/query/cms/post_comment_test.exs +++ b/test/mastani_server_web/query/cms/post_comment_test.exs @@ -18,7 +18,7 @@ defmodule MastaniServer.Test.Query.PostComment do describe "[post comment]" do @query """ query($id: ID!, $filter: CommentsFilter!) { - comments(id: $id, filter: $filter) { + pagedComments(id: $id, filter: $filter) { entries { id likesCount @@ -41,7 +41,7 @@ defmodule MastaniServer.Test.Query.PostComment do end) variables = %{id: post.id, filter: %{page: 1, size: 10}} - results = guest_conn |> query_result(@query, variables, "comments") + results = guest_conn |> query_result(@query, variables, "pagedComments") assert results |> is_valid_pagination? assert results["totalCount"] == 30 @@ -73,7 +73,7 @@ defmodule MastaniServer.Test.Query.PostComment do {:ok, _} = CMS.like_comment(:post_comment, comment_1.id, user_4) variables = %{id: post.id, filter: %{page: 1, size: 10, sort: "MOST_LIKES"}} - results = guest_conn |> query_result(@query, variables, "comments") + results = guest_conn |> query_result(@query, variables, "pagedComments") entries = results["entries"] assert entries |> Enum.at(0) |> Map.get("id") == to_string(comment_3.id) @@ -109,7 +109,7 @@ defmodule MastaniServer.Test.Query.PostComment do {:ok, _} = CMS.dislike_comment(:post_comment, comment_1.id, user_4) variables = %{id: post.id, filter: %{page: 1, size: 10, sort: "MOST_DISLIKES"}} - results = guest_conn |> query_result(@query, variables, "comments") + results = guest_conn |> query_result(@query, variables, "pagedComments") entries = results["entries"] assert entries |> Enum.at(0) |> Map.get("id") == to_string(comment_3.id) @@ -121,7 +121,7 @@ defmodule MastaniServer.Test.Query.PostComment do @query """ query($id: ID!, $filter: CommentsFilter!) { - comments(id: $id, filter: $filter) { + pagedComments(id: $id, filter: $filter) { entries { id viewerHasLiked @@ -137,7 +137,7 @@ defmodule MastaniServer.Test.Query.PostComment do {:ok, _like} = CMS.like_comment(:post_comment, comment.id, user) variables = %{id: post.id, filter: %{page: 1, size: 10}} - results = user_conn |> query_result(@query, variables, "comments") + results = user_conn |> query_result(@query, variables, "pagedComments") found = results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() @@ -145,7 +145,7 @@ defmodule MastaniServer.Test.Query.PostComment do assert found["viewerHasLiked"] == false own_like_conn = simu_conn(:user, user) - results = own_like_conn |> query_result(@query, variables, "comments") + results = own_like_conn |> query_result(@query, variables, "pagedComments") found = results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() @@ -155,7 +155,7 @@ defmodule MastaniServer.Test.Query.PostComment do @query """ query($id: ID!, $filter: PagedFilter!) { - comments(id: $id, filter: $filter) { + pagedComments(id: $id, filter: $filter) { entries { id body @@ -180,7 +180,7 @@ defmodule MastaniServer.Test.Query.PostComment do {:ok, _like} = CMS.like_comment(:post_comment, comment.id, user) variables = %{id: post.id, filter: %{page: 1, size: 10}} - results = guest_conn |> query_result(@query, variables, "comments") + results = guest_conn |> query_result(@query, variables, "pagedComments") found = results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() @@ -195,7 +195,7 @@ defmodule MastaniServer.Test.Query.PostComment do @query """ query($id: ID!, $filter: PagedFilter!) { - comments(id: $id, filter: $filter) { + pagedComments(id: $id, filter: $filter) { entries { id body @@ -220,7 +220,7 @@ defmodule MastaniServer.Test.Query.PostComment do {:ok, _like} = CMS.dislike_comment(:post_comment, comment.id, user) variables = %{id: post.id, filter: %{page: 1, size: 10}} - results = guest_conn |> query_result(@query, variables, "comments") + results = guest_conn |> query_result(@query, variables, "pagedComments") found = results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() @@ -235,7 +235,7 @@ defmodule MastaniServer.Test.Query.PostComment do @query """ query($id: ID!, $filter: PagedFilter!) { - comments(id: $id, filter: $filter) { + pagedComments(id: $id, filter: $filter) { entries { id body @@ -260,7 +260,7 @@ defmodule MastaniServer.Test.Query.PostComment do {:ok, reply} = CMS.reply_comment(:post, comment.id, "reply body", user) variables = %{id: post.id, filter: %{page: 1, size: 10}} - results = guest_conn |> query_result(@query, variables, "comments") + results = guest_conn |> query_result(@query, variables, "pagedComments") found = results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() From 505066184e565c61a3d1282d2175cfacf42ad4d3 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 8 Oct 2018 13:33:35 +0800 Subject: [PATCH 045/129] test(repo tests): badic query tests --- lib/mastani_server/cms/repo.ex | 13 +- lib/mastani_server/cms/utils/loader.ex | 60 ++++- .../query/cms/repo_comment_test.exs | 213 ++++++++++++++++++ 3 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 test/mastani_server_web/query/cms/repo_comment_test.exs diff --git a/lib/mastani_server/cms/repo.ex b/lib/mastani_server/cms/repo.ex index d09f85226..aedd6da30 100644 --- a/lib/mastani_server/cms/repo.ex +++ b/lib/mastani_server/cms/repo.ex @@ -4,7 +4,16 @@ defmodule MastaniServer.CMS.Repo do use Ecto.Schema import Ecto.Changeset - alias MastaniServer.CMS.{Author, Community, RepoContributor, RepoLang, RepoCommunityFlag, Tag} + + alias MastaniServer.CMS.{ + Author, + Community, + RepoComment, + RepoContributor, + RepoLang, + RepoCommunityFlag, + Tag + } @required_fields ~w(title owner_name owner_url repo_url desc readme star_count issues_count prs_count fork_count watch_count)a @optional_fields ~w(last_fetch_time homepage_url release_tag license) @@ -42,6 +51,8 @@ defmodule MastaniServer.CMS.Repo do field(:last_fetch_time, :utc_datetime) + has_many(:comments, {"repos_comments", RepoComment}) + many_to_many( :tags, Tag, diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index ded885872..a976a13c7 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -32,7 +32,11 @@ defmodule MastaniServer.CMS.Utils.Loader do VideoStar, VideoCommentReply, VideoCommentDislike, - VideoCommentLike + VideoCommentLike, + # repo + RepoCommentReply, + RepoCommentLike, + RepoCommentDislike } def data, do: Dataloader.Ecto.new(Repo, query: &query/2, run_batch: &run_batch/5) @@ -376,6 +380,60 @@ defmodule MastaniServer.CMS.Utils.Loader do # ---- video ------ + # --- repo comments ------ + def query({"repos_comments_replies", RepoCommentReply}, %{count: _}) do + RepoCommentReply + |> group_by([c], c.repo_comment_id) + |> select([c], count(c.id)) + end + + def query({"repos_comments_replies", RepoCommentReply}, %{filter: filter}) do + RepoCommentReply + |> QueryBuilder.load_inner_replies(filter) + end + + def query({"repos_comments_replies", RepoCommentReply}, %{reply_to: _}) do + RepoCommentReply + |> join(:inner, [c], r in assoc(c, :repo_comment)) + |> select([c, r], r) + end + + def query({"repos_comments_likes", RepoCommentLike}, %{count: _}) do + RepoCommentLike + |> group_by([f], f.repo_comment_id) + |> select([f], count(f.id)) + end + + def query({"repos_comments_likes", RepoCommentLike}, %{viewer_did: _, cur_user: cur_user}) do + RepoCommentLike |> where([f], f.user_id == ^cur_user.id) + end + + def query({"repos_comments_likes", RepoCommentLike}, %{filter: _filter} = args) do + RepoCommentLike + |> QueryBuilder.members_pack(args) + end + + def query({"repos_comments_dislikes", RepoCommentDislike}, %{count: _}) do + RepoCommentDislike + |> group_by([f], f.repo_comment_id) + |> select([f], count(f.id)) + end + + # component dislikes + def query({"repos_comments_dislikes", RepoCommentDislike}, %{ + viewer_did: _, + cur_user: cur_user + }) do + RepoCommentDislike |> where([f], f.user_id == ^cur_user.id) + end + + def query({"repos_comments_dislikes", RepoCommentDislike}, %{filter: _filter} = args) do + RepoCommentDislike + |> QueryBuilder.members_pack(args) + end + + # --- repo ------ + # default loader def query(queryable, _args) do # IO.inspect(queryable, label: "default loader") diff --git a/test/mastani_server_web/query/cms/repo_comment_test.exs b/test/mastani_server_web/query/cms/repo_comment_test.exs new file mode 100644 index 000000000..434965fcb --- /dev/null +++ b/test/mastani_server_web/query/cms/repo_comment_test.exs @@ -0,0 +1,213 @@ +defmodule MastaniServer.Test.Query.RepoComment do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, repo} = db_insert(:repo) + {:ok, user} = db_insert(:user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user) + + {:ok, ~m(user_conn guest_conn repo user)a} + end + + # TODO: user can get specific user's replies :list_replies + describe "[repo comment]" do + @query """ + query($thread: CmsThread, $id: ID!, $filter: CommentsFilter!) { + pagedComments(thread: $thread, id: $id, filter: $filter) { + entries { + id + body + likesCount + dislikesCount + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + @tag :wip + test "guest user can get a paged comment", ~m(guest_conn repo user)a do + body = "test comment" + + Enum.reduce(1..30, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:repo, repo.id, body, user) + + acc ++ [value] + end) + + variables = %{thread: "REPO", id: repo.id, filter: %{page: 1, size: 10}} + results = guest_conn |> query_result(@query, variables, "pagedComments") + + assert results |> is_valid_pagination? + assert results["totalCount"] == 30 + end + + @tag :wip2 + test "MOST_LIKES filter should work", ~m(guest_conn repo user)a do + body = "test comment" + + comments = + Enum.reduce(1..10, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:repo, repo.id, body, user) + + acc ++ [value] + end) + + [comment_1, _comment_2, comment_3, _comment_last] = comments |> firstn_and_last(3) + + {:ok, [user_1, user_2, user_3, user_4, user_5]} = db_insert_multi(:user, 5) + + # comment_3 is most likes + {:ok, _} = CMS.like_comment(:repo_comment, comment_3.id, user_1) + {:ok, _} = CMS.like_comment(:repo_comment, comment_3.id, user_2) + {:ok, _} = CMS.like_comment(:repo_comment, comment_3.id, user_3) + {:ok, _} = CMS.like_comment(:repo_comment, comment_3.id, user_4) + {:ok, _} = CMS.like_comment(:repo_comment, comment_3.id, user_5) + + {:ok, _} = CMS.like_comment(:repo_comment, comment_1.id, user_1) + {:ok, _} = CMS.like_comment(:repo_comment, comment_1.id, user_2) + {:ok, _} = CMS.like_comment(:repo_comment, comment_1.id, user_3) + {:ok, _} = CMS.like_comment(:repo_comment, comment_1.id, user_4) + + variables = %{ + thread: "REPO", + id: repo.id, + filter: %{page: 1, size: 10, sort: "MOST_LIKES"} + } + + results = guest_conn |> query_result(@query, variables, "pagedComments") + + entries = results["entries"] + + assert entries |> Enum.at(0) |> Map.get("id") == to_string(comment_3.id) + assert entries |> Enum.at(0) |> Map.get("likesCount") == 5 + + assert entries |> Enum.at(1) |> Map.get("id") == to_string(comment_1.id) + assert entries |> Enum.at(1) |> Map.get("likesCount") == 4 + end + + @tag :wip + test "MOST_DISLIKES filter should work", ~m(guest_conn repo user)a do + body = "test comment" + + comments = + Enum.reduce(1..10, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:repo, repo.id, body, user) + + acc ++ [value] + end) + + [comment_1, _comment_2, comment_3, _comment_last] = comments |> firstn_and_last(3) + {:ok, [user_1, user_2, user_3, user_4, user_5]} = db_insert_multi(:user, 5) + + # comment_3 is most likes + {:ok, _} = CMS.dislike_comment(:repo_comment, comment_3.id, user_1) + {:ok, _} = CMS.dislike_comment(:repo_comment, comment_3.id, user_2) + {:ok, _} = CMS.dislike_comment(:repo_comment, comment_3.id, user_3) + {:ok, _} = CMS.dislike_comment(:repo_comment, comment_3.id, user_4) + {:ok, _} = CMS.dislike_comment(:repo_comment, comment_3.id, user_5) + + {:ok, _} = CMS.dislike_comment(:repo_comment, comment_1.id, user_1) + {:ok, _} = CMS.dislike_comment(:repo_comment, comment_1.id, user_2) + {:ok, _} = CMS.dislike_comment(:repo_comment, comment_1.id, user_3) + {:ok, _} = CMS.dislike_comment(:repo_comment, comment_1.id, user_4) + + variables = %{ + thread: "REPO", + id: repo.id, + filter: %{page: 1, size: 10, sort: "MOST_DISLIKES"} + } + + results = guest_conn |> query_result(@query, variables, "pagedComments") + entries = results["entries"] + + assert entries |> Enum.at(0) |> Map.get("id") == to_string(comment_3.id) + assert entries |> Enum.at(0) |> Map.get("dislikesCount") == 5 + + assert entries |> Enum.at(1) |> Map.get("id") == to_string(comment_1.id) + assert entries |> Enum.at(1) |> Map.get("dislikesCount") == 4 + end + + @query """ + query($thread: CmsThread, $id: ID!, $filter: CommentsFilter!) { + pagedComments(thread: $thread, id: $id, filter: $filter) { + entries { + id + viewerHasLiked + } + } + } + """ + @tag :wip + test "login user can get hasLiked feedBack", ~m(user_conn repo user)a do + body = "test comment" + + {:ok, comment} = CMS.create_comment(:repo, repo.id, body, user) + + {:ok, _like} = CMS.like_comment(:repo_comment, comment.id, user) + + variables = %{thread: "REPO", id: repo.id, filter: %{page: 1, size: 10}} + results = user_conn |> query_result(@query, variables, "pagedComments") + + found = + results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() + + assert found["viewerHasLiked"] == false + + own_like_conn = simu_conn(:user, user) + results = own_like_conn |> query_result(@query, variables, "pagedComments") + + found = + results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() + + assert found["viewerHasLiked"] == true + end + + @query """ + query($thread: CmsThread, $id: ID!, $filter: PagedFilter!) { + pagedComments(thread: $thread, id: $id, filter: $filter) { + entries { + id + body + replyTo { + id + body + } + repliesCount + replies { + id + body + } + } + } + } + """ + @tag :wip + test "guest user can get replies info", ~m(guest_conn repo user)a do + body = "test comment" + {:ok, comment} = CMS.create_comment(:repo, repo.id, body, user) + + {:ok, reply} = CMS.reply_comment(:repo, comment.id, "reply body", user) + + variables = %{thread: "REPO", id: repo.id, filter: %{page: 1, size: 10}} + results = guest_conn |> query_result(@query, variables, "pagedComments") + + found = + results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() + + found_reply = + results["entries"] |> Enum.filter(&(&1["id"] == to_string(reply.id))) |> List.first() + + assert found["repliesCount"] == 1 + assert found["replies"] |> Enum.any?(&(&1["id"] == to_string(reply.id))) + assert found["replyTo"] == nil + assert found_reply["replyTo"] |> Map.get("id") == to_string(comment.id) + end + end +end From fef079317f1c30e3593b2632b81078f737ea642a Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 8 Oct 2018 14:05:03 +0800 Subject: [PATCH 046/129] test: add mutation tests for video/repo comments --- .../mutation/cms/repo_comment_test.exs | 202 ++++++++++++++++++ .../mutation/cms/video_comment_test.exs | 202 ++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 test/mastani_server_web/mutation/cms/repo_comment_test.exs create mode 100644 test/mastani_server_web/mutation/cms/video_comment_test.exs diff --git a/test/mastani_server_web/mutation/cms/repo_comment_test.exs b/test/mastani_server_web/mutation/cms/repo_comment_test.exs new file mode 100644 index 000000000..6e941ede3 --- /dev/null +++ b/test/mastani_server_web/mutation/cms/repo_comment_test.exs @@ -0,0 +1,202 @@ +defmodule MastaniServer.Test.Mutation.RepoComment do + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.CMS + + setup do + {:ok, repo} = db_insert(:repo) + {:ok, user} = db_insert(:user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user) + + {:ok, comment} = CMS.create_comment(:repo, repo.id, "test comment", user) + + {:ok, ~m(user_conn guest_conn repo user comment)a} + end + + describe "[repo comment CURD]" do + @create_comment_query """ + mutation($thread: CmsThread, $id: ID!, $body: String!) { + createComment(thread: $thread, id: $id, body: $body) { + id + body + } + } + """ + test "login user can create comment to a repo", ~m(user_conn repo)a do + variables = %{thread: "REPO", id: repo.id, body: "this a comment"} + created = user_conn |> mutation_result(@create_comment_query, variables, "createComment") + + {:ok, found} = ORM.find(CMS.RepoComment, created["id"]) + + assert created["id"] == to_string(found.id) + end + + test "guest user create comment fails", ~m(guest_conn repo)a do + variables = %{thread: "REPO", id: repo.id, body: "this a comment"} + + assert guest_conn + |> mutation_get_error?(@create_comment_query, variables, ecode(:account_login)) + end + + @delete_comment_query """ + mutation($thread: CmsThread, $id: ID!) { + deleteComment(thread: $thread, id: $id) { + id + body + } + } + """ + @tag :wip + test "comment owner can delete comment", ~m(user repo)a do + variables = %{thread: "REPO", id: repo.id, body: "this a comment"} + + user_conn = simu_conn(:user, user) + created = user_conn |> mutation_result(@create_comment_query, variables, "createComment") + + variables = %{thread: "REPO", id: created["id"]} + deleted = user_conn |> mutation_result(@delete_comment_query, variables, "deleteComment") + + assert deleted["id"] == created["id"] + end + + @tag :wip + test "unauth user delete comment fails", ~m(user_conn guest_conn repo)a do + variables = %{thread: "REPO", id: repo.id, body: "this a comment"} + {:ok, owner} = db_insert(:user) + owner_conn = simu_conn(:user, owner) + created = owner_conn |> mutation_result(@create_comment_query, variables, "createComment") + + variables = %{thread: "REPO", id: created["id"]} + rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) + + assert user_conn |> mutation_get_error?(@delete_comment_query, variables, ecode(:passport)) + + assert guest_conn + |> mutation_get_error?(@delete_comment_query, variables, ecode(:account_login)) + + assert rule_conn |> mutation_get_error?(@delete_comment_query, variables, ecode(:passport)) + end + + @reply_comment_query """ + mutation($thread: CmsThread!, $id: ID!, $body: String!) { + replyComment(thread: $thread, id: $id, body: $body) { + id + body + replyTo { + id + body + } + } + } + """ + test "login user can reply to a exsit comment", ~m(user_conn comment)a do + variables = %{thread: "REPO", id: comment.id, body: "this a reply"} + replied = user_conn |> mutation_result(@reply_comment_query, variables, "replyComment") + + assert replied["replyTo"] |> Map.get("id") == to_string(comment.id) + end + + test "guest user reply comment fails", ~m(guest_conn comment)a do + variables = %{thread: "REPO", id: comment.id, body: "this a reply"} + + assert guest_conn |> mutation_get_error?(@reply_comment_query, variables) + end + + test "TODO owner can NOT delete comment when comment has replies" do + end + + test "TODO owner can NOT edit comment when comment has replies" do + end + + test "TODO owner can NOT delete comment when comment has created after 3 hours" do + end + end + + describe "[repo comment reactions]" do + @like_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + likeComment(thread: $thread, id: $id) { + id + } + } + """ + test "login user can like a comment", ~m(user_conn comment)a do + variables = %{thread: "REPO_COMMENT", id: comment.id} + user_conn |> mutation_result(@like_comment_query, variables, "likeComment") + + {:ok, found} = CMS.RepoComment |> ORM.find(comment.id, preload: :likes) + + assert found.likes |> Enum.any?(&(&1.repo_comment_id == comment.id)) + end + + @undo_like_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + undoLikeComment(thread: $thread, id: $id) { + id + } + } + """ + test "login user can undo a like action to comment", ~m(user comment)a do + variables = %{thread: "REPO_COMMENT", id: comment.id} + user_conn = simu_conn(:user, user) + user_conn |> mutation_result(@like_comment_query, variables, "likeComment") + + {:ok, found} = CMS.RepoComment |> ORM.find(comment.id, preload: :likes) + assert found.likes |> Enum.any?(&(&1.repo_comment_id == comment.id)) + + user_conn |> mutation_result(@undo_like_comment_query, variables, "undoLikeComment") + + {:ok, found} = CMS.RepoComment |> ORM.find(comment.id, preload: :likes) + assert false == found.likes |> Enum.any?(&(&1.repo_comment_id == comment.id)) + end + + @dislike_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + dislikeComment(thread: $thread, id: $id) { + id + } + } + """ + test "login user can dislike a comment", ~m(user_conn comment)a do + variables = %{thread: "REPO_COMMENT", id: comment.id} + user_conn |> mutation_result(@dislike_comment_query, variables, "dislikeComment") + + {:ok, found} = CMS.RepoComment |> ORM.find(comment.id, preload: :dislikes) + + assert found.dislikes |> Enum.any?(&(&1.repo_comment_id == comment.id)) + end + + @undo_dislike_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + undoDislikeComment(thread: $thread, id: $id) { + id + } + } + """ + test "login user can undo dislike a comment", ~m(user comment)a do + variables = %{thread: "REPO_COMMENT", id: comment.id} + user_conn = simu_conn(:user, user) + user_conn |> mutation_result(@dislike_comment_query, variables, "dislikeComment") + {:ok, found} = CMS.RepoComment |> ORM.find(comment.id, preload: :dislikes) + assert found.dislikes |> Enum.any?(&(&1.repo_comment_id == comment.id)) + + user_conn |> mutation_result(@undo_dislike_comment_query, variables, "undoDislikeComment") + + {:ok, found} = CMS.RepoComment |> ORM.find(comment.id, preload: :dislikes) + assert false == found.dislikes |> Enum.any?(&(&1.repo_comment_id == comment.id)) + end + + test "unloged user do/undo like/dislike comment fails", ~m(guest_conn comment)a do + variables = %{thread: "REPO_COMMENT", id: comment.id} + + assert guest_conn |> mutation_get_error?(@like_comment_query, variables) + assert guest_conn |> mutation_get_error?(@dislike_comment_query, variables) + + assert guest_conn |> mutation_get_error?(@undo_like_comment_query, variables) + assert guest_conn |> mutation_get_error?(@undo_dislike_comment_query, variables) + end + end +end diff --git a/test/mastani_server_web/mutation/cms/video_comment_test.exs b/test/mastani_server_web/mutation/cms/video_comment_test.exs new file mode 100644 index 000000000..fc9de2177 --- /dev/null +++ b/test/mastani_server_web/mutation/cms/video_comment_test.exs @@ -0,0 +1,202 @@ +defmodule MastaniServer.Test.Mutation.VideoComment do + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.CMS + + setup do + {:ok, video} = db_insert(:video) + {:ok, user} = db_insert(:user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user) + + {:ok, comment} = CMS.create_comment(:video, video.id, "test comment", user) + + {:ok, ~m(user_conn guest_conn video user comment)a} + end + + describe "[video comment CURD]" do + @create_comment_query """ + mutation($thread: CmsThread, $id: ID!, $body: String!) { + createComment(thread: $thread, id: $id, body: $body) { + id + body + } + } + """ + test "login user can create comment to a video", ~m(user_conn video)a do + variables = %{thread: "VIDEO", id: video.id, body: "this a comment"} + created = user_conn |> mutation_result(@create_comment_query, variables, "createComment") + + {:ok, found} = ORM.find(CMS.VideoComment, created["id"]) + + assert created["id"] == to_string(found.id) + end + + test "guest user create comment fails", ~m(guest_conn video)a do + variables = %{thread: "VIDEO", id: video.id, body: "this a comment"} + + assert guest_conn + |> mutation_get_error?(@create_comment_query, variables, ecode(:account_login)) + end + + @delete_comment_query """ + mutation($thread: CmsThread, $id: ID!) { + deleteComment(thread: $thread, id: $id) { + id + body + } + } + """ + @tag :wip + test "comment owner can delete comment", ~m(user video)a do + variables = %{thread: "VIDEO", id: video.id, body: "this a comment"} + + user_conn = simu_conn(:user, user) + created = user_conn |> mutation_result(@create_comment_query, variables, "createComment") + + variables = %{thread: "VIDEO", id: created["id"]} + deleted = user_conn |> mutation_result(@delete_comment_query, variables, "deleteComment") + + assert deleted["id"] == created["id"] + end + + @tag :wip + test "unauth user delete comment fails", ~m(user_conn guest_conn video)a do + variables = %{thread: "VIDEO", id: video.id, body: "this a comment"} + {:ok, owner} = db_insert(:user) + owner_conn = simu_conn(:user, owner) + created = owner_conn |> mutation_result(@create_comment_query, variables, "createComment") + + variables = %{thread: "VIDEO", id: created["id"]} + rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) + + assert user_conn |> mutation_get_error?(@delete_comment_query, variables, ecode(:passport)) + + assert guest_conn + |> mutation_get_error?(@delete_comment_query, variables, ecode(:account_login)) + + assert rule_conn |> mutation_get_error?(@delete_comment_query, variables, ecode(:passport)) + end + + @reply_comment_query """ + mutation($thread: CmsThread!, $id: ID!, $body: String!) { + replyComment(thread: $thread, id: $id, body: $body) { + id + body + replyTo { + id + body + } + } + } + """ + test "login user can reply to a exsit comment", ~m(user_conn comment)a do + variables = %{thread: "VIDEO", id: comment.id, body: "this a reply"} + replied = user_conn |> mutation_result(@reply_comment_query, variables, "replyComment") + + assert replied["replyTo"] |> Map.get("id") == to_string(comment.id) + end + + test "guest user reply comment fails", ~m(guest_conn comment)a do + variables = %{thread: "VIDEO", id: comment.id, body: "this a reply"} + + assert guest_conn |> mutation_get_error?(@reply_comment_query, variables) + end + + test "TODO owner can NOT delete comment when comment has replies" do + end + + test "TODO owner can NOT edit comment when comment has replies" do + end + + test "TODO owner can NOT delete comment when comment has created after 3 hours" do + end + end + + describe "[video comment reactions]" do + @like_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + likeComment(thread: $thread, id: $id) { + id + } + } + """ + test "login user can like a comment", ~m(user_conn comment)a do + variables = %{thread: "VIDEO_COMMENT", id: comment.id} + user_conn |> mutation_result(@like_comment_query, variables, "likeComment") + + {:ok, found} = CMS.VideoComment |> ORM.find(comment.id, preload: :likes) + + assert found.likes |> Enum.any?(&(&1.video_comment_id == comment.id)) + end + + @undo_like_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + undoLikeComment(thread: $thread, id: $id) { + id + } + } + """ + test "login user can undo a like action to comment", ~m(user comment)a do + variables = %{thread: "VIDEO_COMMENT", id: comment.id} + user_conn = simu_conn(:user, user) + user_conn |> mutation_result(@like_comment_query, variables, "likeComment") + + {:ok, found} = CMS.VideoComment |> ORM.find(comment.id, preload: :likes) + assert found.likes |> Enum.any?(&(&1.video_comment_id == comment.id)) + + user_conn |> mutation_result(@undo_like_comment_query, variables, "undoLikeComment") + + {:ok, found} = CMS.VideoComment |> ORM.find(comment.id, preload: :likes) + assert false == found.likes |> Enum.any?(&(&1.video_comment_id == comment.id)) + end + + @dislike_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + dislikeComment(thread: $thread, id: $id) { + id + } + } + """ + test "login user can dislike a comment", ~m(user_conn comment)a do + variables = %{thread: "VIDEO_COMMENT", id: comment.id} + user_conn |> mutation_result(@dislike_comment_query, variables, "dislikeComment") + + {:ok, found} = CMS.VideoComment |> ORM.find(comment.id, preload: :dislikes) + + assert found.dislikes |> Enum.any?(&(&1.video_comment_id == comment.id)) + end + + @undo_dislike_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + undoDislikeComment(thread: $thread, id: $id) { + id + } + } + """ + test "login user can undo dislike a comment", ~m(user comment)a do + variables = %{thread: "VIDEO_COMMENT", id: comment.id} + user_conn = simu_conn(:user, user) + user_conn |> mutation_result(@dislike_comment_query, variables, "dislikeComment") + {:ok, found} = CMS.VideoComment |> ORM.find(comment.id, preload: :dislikes) + assert found.dislikes |> Enum.any?(&(&1.video_comment_id == comment.id)) + + user_conn |> mutation_result(@undo_dislike_comment_query, variables, "undoDislikeComment") + + {:ok, found} = CMS.VideoComment |> ORM.find(comment.id, preload: :dislikes) + assert false == found.dislikes |> Enum.any?(&(&1.video_comment_id == comment.id)) + end + + test "unloged user do/undo like/dislike comment fails", ~m(guest_conn comment)a do + variables = %{thread: "VIDEO_COMMENT", id: comment.id} + + assert guest_conn |> mutation_get_error?(@like_comment_query, variables) + assert guest_conn |> mutation_get_error?(@dislike_comment_query, variables) + + assert guest_conn |> mutation_get_error?(@undo_like_comment_query, variables) + assert guest_conn |> mutation_get_error?(@undo_dislike_comment_query, variables) + end + end +end From 5f6bee210da6ad67942d0c7af4b12608458461f7 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 8 Oct 2018 15:20:58 +0800 Subject: [PATCH 047/129] feat(job comment): add like / dislike & test --- .../cms/delegates/comment_reaction.ex | 1 + lib/mastani_server/cms/job_comment.ex | 7 +- lib/mastani_server/cms/job_comment_dislike.ex | 29 +++++ lib/mastani_server/cms/job_comment_like.ex | 29 +++++ lib/mastani_server/cms/utils/loader.ex | 42 +++++- lib/mastani_server/cms/utils/matcher.ex | 24 ++-- ...181008062314_add_likes_to_job_comments.exs | 14 ++ ...008062922_add_dislikes_to_job_comments.exs | 14 ++ test/mastani_server/cms/job_comment_test.exs | 62 +++++++++ .../mutation/cms/job_comment_test.exs | 90 +++++++++++++ .../query/cms/job_comment_test.exs | 123 ++++++++++++++++++ 11 files changed, 418 insertions(+), 17 deletions(-) create mode 100644 lib/mastani_server/cms/job_comment_dislike.ex create mode 100644 lib/mastani_server/cms/job_comment_like.ex create mode 100644 priv/repo/migrations/20181008062314_add_likes_to_job_comments.exs create mode 100644 priv/repo/migrations/20181008062922_add_dislikes_to_job_comments.exs diff --git a/lib/mastani_server/cms/delegates/comment_reaction.ex b/lib/mastani_server/cms/delegates/comment_reaction.ex index 2240b29f0..a478d2d86 100644 --- a/lib/mastani_server/cms/delegates/comment_reaction.ex +++ b/lib/mastani_server/cms/delegates/comment_reaction.ex @@ -21,6 +21,7 @@ defmodule MastaniServer.CMS.Delegate.CommentReaction do end defp merge_thread_comment_id(:post_comment, comment_id), do: %{post_comment_id: comment_id} + defp merge_thread_comment_id(:job_comment, comment_id), do: %{job_comment_id: comment_id} defp merge_thread_comment_id(:video_comment, comment_id), do: %{video_comment_id: comment_id} defp merge_thread_comment_id(:repo_comment, comment_id), do: %{repo_comment_id: comment_id} diff --git a/lib/mastani_server/cms/job_comment.ex b/lib/mastani_server/cms/job_comment.ex index 3e4b80ca8..b2633c936 100644 --- a/lib/mastani_server/cms/job_comment.ex +++ b/lib/mastani_server/cms/job_comment.ex @@ -6,7 +6,7 @@ defmodule MastaniServer.CMS.JobComment do import Ecto.Changeset alias MastaniServer.Accounts - alias MastaniServer.CMS.{Job, JobCommentReply} + alias MastaniServer.CMS.{Job, JobCommentReply, JobCommentLike, JobCommentDislike} @required_fields ~w(body author_id job_id floor)a @optional_fields ~w(reply_id)a @@ -18,8 +18,11 @@ defmodule MastaniServer.CMS.JobComment do belongs_to(:author, Accounts.User, foreign_key: :author_id) belongs_to(:job, Job, foreign_key: :job_id) belongs_to(:reply_to, JobComment, foreign_key: :reply_id) - # belongs_to(:reply_to, JobComment, foreign_key: :job_id) + has_many(:replies, {"jobs_comments_replies", JobCommentReply}) + has_many(:likes, {"jobs_comments_likes", JobCommentLike}) + has_many(:dislikes, {"jobs_comments_dislikes", JobCommentDislike}) + timestamps(type: :utc_datetime) end diff --git a/lib/mastani_server/cms/job_comment_dislike.ex b/lib/mastani_server/cms/job_comment_dislike.ex new file mode 100644 index 000000000..e3d005532 --- /dev/null +++ b/lib/mastani_server/cms/job_comment_dislike.ex @@ -0,0 +1,29 @@ +defmodule MastaniServer.CMS.JobCommentDislike do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.JobComment + + @required_fields ~w(job_comment_id user_id)a + + @type t :: %JobCommentDislike{} + schema "jobs_comments_dislikes" do + belongs_to(:user, Accounts.User, foreign_key: :user_id) + belongs_to(:job_comment, JobComment, foreign_key: :job_comment_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%JobCommentDislike{} = job_comment_dislike, attrs) do + job_comment_dislike + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:job_comment_id) + |> foreign_key_constraint(:user_id) + |> unique_constraint(:user_id, name: :jobs_comments_dislikes_user_id_job_comment_id_index) + end +end diff --git a/lib/mastani_server/cms/job_comment_like.ex b/lib/mastani_server/cms/job_comment_like.ex new file mode 100644 index 000000000..626c84a5c --- /dev/null +++ b/lib/mastani_server/cms/job_comment_like.ex @@ -0,0 +1,29 @@ +defmodule MastaniServer.CMS.JobCommentLike do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.JobComment + + @required_fields ~w(job_comment_id user_id)a + + @type t :: %JobCommentLike{} + schema "jobs_comments_likes" do + belongs_to(:user, Accounts.User, foreign_key: :user_id) + belongs_to(:job_comment, JobComment, foreign_key: :job_comment_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%JobCommentLike{} = job_comment_like, attrs) do + job_comment_like + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:job_comment_id) + |> foreign_key_constraint(:user_id) + |> unique_constraint(:user_id, name: :jobs_comments_likes_user_id_job_comment_id_index) + end +end diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index a976a13c7..b2350af5e 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -25,6 +25,8 @@ defmodule MastaniServer.CMS.Utils.Loader do JobFavorite, JobStar, JobCommentReply, + JobCommentDislike, + JobCommentLike, # job comment # JobComment, # Video @@ -324,6 +326,37 @@ defmodule MastaniServer.CMS.Utils.Loader do |> select([c, r], r) end + def query({"jobs_comments_likes", JobCommentLike}, %{count: _}) do + JobCommentLike + |> group_by([f], f.job_comment_id) + |> select([f], count(f.id)) + end + + def query({"jobs_comments_likes", JobCommentLike}, %{viewer_did: _, cur_user: cur_user}) do + JobCommentLike |> where([f], f.user_id == ^cur_user.id) + end + + def query({"jobs_comments_likes", JobCommentLike}, %{filter: _filter} = args) do + JobCommentLike |> QueryBuilder.members_pack(args) + end + + def query({"jobs_comments_dislikes", JobCommentDislike}, %{count: _}) do + JobCommentDislike + |> group_by([f], f.job_comment_id) + |> select([f], count(f.id)) + end + + def query({"jobs_comments_dislikes", JobCommentDislike}, %{ + viewer_did: _, + cur_user: cur_user + }) do + JobCommentDislike |> where([f], f.user_id == ^cur_user.id) + end + + def query({"jobs_comments_dislikes", JobCommentDislike}, %{filter: _filter} = args) do + JobCommentDislike |> QueryBuilder.members_pack(args) + end + # ---- job ------ # ---- video comments ------ @@ -334,8 +367,7 @@ defmodule MastaniServer.CMS.Utils.Loader do end def query({"videos_comments_replies", VideoCommentReply}, %{filter: filter}) do - VideoCommentReply - |> QueryBuilder.load_inner_replies(filter) + VideoCommentReply |> QueryBuilder.load_inner_replies(filter) end def query({"videos_comments_replies", VideoCommentReply}, %{reply_to: _}) do @@ -435,9 +467,5 @@ defmodule MastaniServer.CMS.Utils.Loader do # --- repo ------ # default loader - def query(queryable, _args) do - # IO.inspect(queryable, label: "default loader") - # IO.inspect(args, label: "default args") - queryable - end + def query(queryable, _args), do: queryable end diff --git a/lib/mastani_server/cms/utils/matcher.ex b/lib/mastani_server/cms/utils/matcher.ex index 7d09736cb..5fdf353be 100644 --- a/lib/mastani_server/cms/utils/matcher.ex +++ b/lib/mastani_server/cms/utils/matcher.ex @@ -26,6 +26,8 @@ defmodule MastaniServer.CMS.Utils.Matcher do # commtnes reaction PostCommentLike, PostCommentDislike, + JobCommentLike, + JobCommentDislike, VideoCommentLike, VideoCommentDislike, RepoCommentLike, @@ -85,14 +87,20 @@ defmodule MastaniServer.CMS.Utils.Matcher do def match_action(:job, :community), do: {:ok, %{target: Job, reactor: Community, flag: JobCommunityFlag}} + def match_action(:job, :favorite), + do: {:ok, %{target: Job, reactor: JobFavorite, preload: :user}} + def match_action(:job, :star), do: {:ok, %{target: Job, reactor: JobStar, preload: :user}} def match_action(:job, :tag), do: {:ok, %{target: Job, reactor: Tag}} def match_action(:job, :comment), do: {:ok, %{target: Job, reactor: JobComment, preload: :author}} - def match_action(:job, :favorite), - do: {:ok, %{target: Job, reactor: JobFavorite, preload: :user}} + def match_action(:job_comment, :like), + do: {:ok, %{target: JobComment, reactor: JobCommentLike}} + + def match_action(:job_comment, :dislike), + do: {:ok, %{target: JobComment, reactor: JobCommentDislike}} ######################################### ## videos ... @@ -102,6 +110,12 @@ defmodule MastaniServer.CMS.Utils.Matcher do def match_action(:video, :community), do: {:ok, %{target: Video, reactor: Community, flag: VideoCommunityFlag}} + def match_action(:video, :favorite), + do: {:ok, %{target: Video, reactor: VideoFavorite, preload: :user}} + + def match_action(:video, :star), + do: {:ok, %{target: Video, reactor: VideoStar, preload: :user}} + def match_action(:video, :comment), do: {:ok, %{target: Video, reactor: VideoComment, preload: :author}} @@ -111,12 +125,6 @@ defmodule MastaniServer.CMS.Utils.Matcher do def match_action(:video_comment, :dislike), do: {:ok, %{target: VideoComment, reactor: VideoCommentDislike}} - def match_action(:video, :favorite), - do: {:ok, %{target: Video, reactor: VideoFavorite, preload: :user}} - - def match_action(:video, :star), - do: {:ok, %{target: Video, reactor: VideoStar, preload: :user}} - ######################################### ## repos ... ######################################### diff --git a/priv/repo/migrations/20181008062314_add_likes_to_job_comments.exs b/priv/repo/migrations/20181008062314_add_likes_to_job_comments.exs new file mode 100644 index 000000000..d233d7f73 --- /dev/null +++ b/priv/repo/migrations/20181008062314_add_likes_to_job_comments.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.AddLikesToJobComments do + use Ecto.Migration + + def change do + create table(:jobs_comments_likes) do + add(:user_id, references(:users, on_delete: :delete_all), null: false) + add(:job_comment_id, references(:jobs_comments, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:jobs_comments_likes, [:user_id, :job_comment_id])) + end +end diff --git a/priv/repo/migrations/20181008062922_add_dislikes_to_job_comments.exs b/priv/repo/migrations/20181008062922_add_dislikes_to_job_comments.exs new file mode 100644 index 000000000..369e5bfb4 --- /dev/null +++ b/priv/repo/migrations/20181008062922_add_dislikes_to_job_comments.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.AddDislikesToJobComments do + use Ecto.Migration + + def change do + create table(:jobs_comments_dislikes) do + add(:user_id, references(:users, on_delete: :delete_all), null: false) + add(:job_comment_id, references(:jobs_comments, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:jobs_comments_dislikes, [:user_id, :job_comment_id])) + end +end diff --git a/test/mastani_server/cms/job_comment_test.exs b/test/mastani_server/cms/job_comment_test.exs index 489ed1c28..036a1592e 100644 --- a/test/mastani_server/cms/job_comment_test.exs +++ b/test/mastani_server/cms/job_comment_test.exs @@ -151,4 +151,66 @@ defmodule MastaniServer.Test.JobComment do assert user3.id == found_reply3 |> List.first() |> Map.get(:author_id) end end + + describe "[comment Reactions]" do + @tag :wip2 + test "user can like a comment", ~m(comment user)a do + {:ok, liked_comment} = CMS.like_comment(:job_comment, comment.id, user) + + {:ok, comment_preload} = ORM.find(JobComment, liked_comment.id, preload: :likes) + + assert comment_preload.likes |> Enum.any?(&(&1.job_comment_id == comment.id)) + end + + @tag :wip + test "user like comment twice fails", ~m(comment user)a do + {:ok, _} = CMS.like_comment(:job_comment, comment.id, user) + {:error, _error} = CMS.like_comment(:job_comment, comment.id, user) + # TODO: fix err_msg later + end + + @tag :wip + test "user can undo a like action", ~m(comment user)a do + {:ok, like} = CMS.like_comment(:job_comment, comment.id, user) + {:ok, _} = CMS.undo_like_comment(:job_comment, comment.id, user) + + {:ok, comment_preload} = ORM.find(JobComment, comment.id, preload: :likes) + assert false == comment_preload.likes |> Enum.any?(&(&1.id == like.id)) + end + + @tag :wip + test "user can dislike a comment", ~m(comment user)a do + {:ok, disliked_comment} = CMS.dislike_comment(:job_comment, comment.id, user) + + {:ok, comment_preload} = ORM.find(JobComment, disliked_comment.id, preload: :dislikes) + + assert comment_preload.dislikes |> Enum.any?(&(&1.job_comment_id == comment.id)) + end + + @tag :wip + test "user can undo a dislike action", ~m(comment user)a do + {:ok, dislike} = CMS.dislike_comment(:job_comment, comment.id, user) + {:ok, _} = CMS.undo_dislike_comment(:job_comment, comment.id, user) + + {:ok, comment_preload} = ORM.find(JobComment, comment.id, preload: :dislikes) + assert false == comment_preload.dislikes |> Enum.any?(&(&1.id == dislike.id)) + end + + @tag :wip + test "user can get paged likes of a job comment", ~m(comment)a do + {:ok, user1} = db_insert(:user) + {:ok, user2} = db_insert(:user) + {:ok, user3} = db_insert(:user) + + {:ok, _like1} = CMS.like_comment(:job_comment, comment.id, user1) + {:ok, _like2} = CMS.like_comment(:job_comment, comment.id, user2) + {:ok, _like3} = CMS.like_comment(:job_comment, comment.id, user3) + + {:ok, results} = CMS.reaction_users(:job_comment, :like, comment.id, %{page: 1, size: 10}) + + assert results.entries |> Enum.any?(&(&1.id == user1.id)) + assert results.entries |> Enum.any?(&(&1.id == user2.id)) + assert results.entries |> Enum.any?(&(&1.id == user3.id)) + end + end end diff --git a/test/mastani_server_web/mutation/cms/job_comment_test.exs b/test/mastani_server_web/mutation/cms/job_comment_test.exs index a2d4ac747..291c8d4e5 100644 --- a/test/mastani_server_web/mutation/cms/job_comment_test.exs +++ b/test/mastani_server_web/mutation/cms/job_comment_test.exs @@ -121,4 +121,94 @@ defmodule MastaniServer.Test.Mutation.JobComment do test "TODO owner can NOT delete comment when comment has created after 3 hours" do end end + + describe "[job comment reactions]" do + @like_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + likeComment(thread: $thread, id: $id) { + id + } + } + """ + @tag :wip + test "login user can like a comment", ~m(user_conn comment)a do + variables = %{thread: "JOB_COMMENT", id: comment.id} + user_conn |> mutation_result(@like_comment_query, variables, "likeComment") + + {:ok, found} = CMS.JobComment |> ORM.find(comment.id, preload: :likes) + + assert found.likes |> Enum.any?(&(&1.job_comment_id == comment.id)) + end + + @undo_like_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + undoLikeComment(thread: $thread, id: $id) { + id + } + } + """ + @tag :wip + test "login user can undo a like action to comment", ~m(user comment)a do + variables = %{thread: "JOB_COMMENT", id: comment.id} + user_conn = simu_conn(:user, user) + user_conn |> mutation_result(@like_comment_query, variables, "likeComment") + + {:ok, found} = CMS.JobComment |> ORM.find(comment.id, preload: :likes) + assert found.likes |> Enum.any?(&(&1.job_comment_id == comment.id)) + + user_conn |> mutation_result(@undo_like_comment_query, variables, "undoLikeComment") + + {:ok, found} = CMS.JobComment |> ORM.find(comment.id, preload: :likes) + assert false == found.likes |> Enum.any?(&(&1.job_comment_id == comment.id)) + end + + @dislike_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + dislikeComment(thread: $thread, id: $id) { + id + } + } + """ + @tag :wip + test "login user can dislike a comment", ~m(user_conn comment)a do + variables = %{thread: "JOB_COMMENT", id: comment.id} + user_conn |> mutation_result(@dislike_comment_query, variables, "dislikeComment") + + {:ok, found} = CMS.JobComment |> ORM.find(comment.id, preload: :dislikes) + + assert found.dislikes |> Enum.any?(&(&1.job_comment_id == comment.id)) + end + + @undo_dislike_comment_query """ + mutation($thread: CmsComment!, $id: ID!) { + undoDislikeComment(thread: $thread, id: $id) { + id + } + } + """ + @tag :wip + test "login user can undo dislike a comment", ~m(user comment)a do + variables = %{thread: "JOB_COMMENT", id: comment.id} + user_conn = simu_conn(:user, user) + user_conn |> mutation_result(@dislike_comment_query, variables, "dislikeComment") + {:ok, found} = CMS.JobComment |> ORM.find(comment.id, preload: :dislikes) + assert found.dislikes |> Enum.any?(&(&1.job_comment_id == comment.id)) + + user_conn |> mutation_result(@undo_dislike_comment_query, variables, "undoDislikeComment") + + {:ok, found} = CMS.JobComment |> ORM.find(comment.id, preload: :dislikes) + assert false == found.dislikes |> Enum.any?(&(&1.job_comment_id == comment.id)) + end + + @tag :wip + test "unloged user do/undo like/dislike comment fails", ~m(guest_conn comment)a do + variables = %{thread: "JOB_COMMENT", id: comment.id} + + assert guest_conn |> mutation_get_error?(@like_comment_query, variables) + assert guest_conn |> mutation_get_error?(@dislike_comment_query, variables) + + assert guest_conn |> mutation_get_error?(@undo_like_comment_query, variables) + assert guest_conn |> mutation_get_error?(@undo_dislike_comment_query, variables) + end + end end diff --git a/test/mastani_server_web/query/cms/job_comment_test.exs b/test/mastani_server_web/query/cms/job_comment_test.exs index 97583afb4..0091de0dd 100644 --- a/test/mastani_server_web/query/cms/job_comment_test.exs +++ b/test/mastani_server_web/query/cms/job_comment_test.exs @@ -21,6 +21,8 @@ defmodule MastaniServer.Test.Query.JobComment do entries { id body + likesCount + dislikesCount } totalPages totalCount @@ -45,6 +47,127 @@ defmodule MastaniServer.Test.Query.JobComment do assert results["totalCount"] == 30 end + @tag :wip + test "MOST_LIKES filter should work", ~m(guest_conn job user)a do + body = "test comment" + + comments = + Enum.reduce(1..10, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:job, job.id, body, user) + + acc ++ [value] + end) + + [comment_1, _comment_2, comment_3, _comment_last] = comments |> firstn_and_last(3) + + {:ok, [user_1, user_2, user_3, user_4, user_5]} = db_insert_multi(:user, 5) + + # comment_3 is most likes + {:ok, _} = CMS.like_comment(:job_comment, comment_3.id, user_1) + {:ok, _} = CMS.like_comment(:job_comment, comment_3.id, user_2) + {:ok, _} = CMS.like_comment(:job_comment, comment_3.id, user_3) + {:ok, _} = CMS.like_comment(:job_comment, comment_3.id, user_4) + {:ok, _} = CMS.like_comment(:job_comment, comment_3.id, user_5) + + {:ok, _} = CMS.like_comment(:job_comment, comment_1.id, user_1) + {:ok, _} = CMS.like_comment(:job_comment, comment_1.id, user_2) + {:ok, _} = CMS.like_comment(:job_comment, comment_1.id, user_3) + {:ok, _} = CMS.like_comment(:job_comment, comment_1.id, user_4) + + variables = %{ + thread: "JOB", + id: job.id, + filter: %{page: 1, size: 10, sort: "MOST_LIKES"} + } + + results = guest_conn |> query_result(@query, variables, "pagedComments") + + entries = results["entries"] + + assert entries |> Enum.at(0) |> Map.get("id") == to_string(comment_3.id) + assert entries |> Enum.at(0) |> Map.get("likesCount") == 5 + + assert entries |> Enum.at(1) |> Map.get("id") == to_string(comment_1.id) + assert entries |> Enum.at(1) |> Map.get("likesCount") == 4 + end + + @tag :wip + test "MOST_DISLIKES filter should work", ~m(guest_conn job user)a do + body = "test comment" + + comments = + Enum.reduce(1..10, [], fn _, acc -> + {:ok, value} = CMS.create_comment(:job, job.id, body, user) + + acc ++ [value] + end) + + [comment_1, _comment_2, comment_3, _comment_last] = comments |> firstn_and_last(3) + {:ok, [user_1, user_2, user_3, user_4, user_5]} = db_insert_multi(:user, 5) + + # comment_3 is most likes + {:ok, _} = CMS.dislike_comment(:job_comment, comment_3.id, user_1) + {:ok, _} = CMS.dislike_comment(:job_comment, comment_3.id, user_2) + {:ok, _} = CMS.dislike_comment(:job_comment, comment_3.id, user_3) + {:ok, _} = CMS.dislike_comment(:job_comment, comment_3.id, user_4) + {:ok, _} = CMS.dislike_comment(:job_comment, comment_3.id, user_5) + + {:ok, _} = CMS.dislike_comment(:job_comment, comment_1.id, user_1) + {:ok, _} = CMS.dislike_comment(:job_comment, comment_1.id, user_2) + {:ok, _} = CMS.dislike_comment(:job_comment, comment_1.id, user_3) + {:ok, _} = CMS.dislike_comment(:job_comment, comment_1.id, user_4) + + variables = %{ + thread: "JOB", + id: job.id, + filter: %{page: 1, size: 10, sort: "MOST_DISLIKES"} + } + + results = guest_conn |> query_result(@query, variables, "pagedComments") + entries = results["entries"] + + assert entries |> Enum.at(0) |> Map.get("id") == to_string(comment_3.id) + assert entries |> Enum.at(0) |> Map.get("dislikesCount") == 5 + + assert entries |> Enum.at(1) |> Map.get("id") == to_string(comment_1.id) + assert entries |> Enum.at(1) |> Map.get("dislikesCount") == 4 + end + + @query """ + query($thread: CmsThread, $id: ID!, $filter: CommentsFilter!) { + pagedComments(thread: $thread, id: $id, filter: $filter) { + entries { + id + viewerHasLiked + } + } + } + """ + @tag :wip + test "login user can get hasLiked feedBack", ~m(user_conn job user)a do + body = "test comment" + + {:ok, comment} = CMS.create_comment(:job, job.id, body, user) + + {:ok, _like} = CMS.like_comment(:job_comment, comment.id, user) + + variables = %{thread: "JOB", id: job.id, filter: %{page: 1, size: 10}} + results = user_conn |> query_result(@query, variables, "pagedComments") + + found = + results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() + + assert found["viewerHasLiked"] == false + + own_like_conn = simu_conn(:user, user) + results = own_like_conn |> query_result(@query, variables, "pagedComments") + + found = + results["entries"] |> Enum.filter(&(&1["id"] == to_string(comment.id))) |> List.first() + + assert found["viewerHasLiked"] == true + end + @query """ query($thread: CmsThread, $id: ID!, $filter: PagedFilter!) { pagedComments(thread: $thread, id: $id, filter: $filter) { From 1dfd5d9935e4f070cde5bf2ce49b56a72fb93421 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 8 Oct 2018 15:23:44 +0800 Subject: [PATCH 048/129] chore(clean up): remove @wip tag --- test/mastani_server/cms/job_comment_test.exs | 6 ------ test/mastani_server/cms/video_reactions_test.exs | 2 -- test/mastani_server_web/mutation/cms/job_comment_test.exs | 5 ----- .../mastani_server_web/mutation/cms/job_reaction_test.exs | 8 -------- .../mutation/cms/post_reaction_test.exs | 8 -------- .../mastani_server_web/mutation/cms/repo_comment_test.exs | 2 -- .../mutation/cms/video_comment_test.exs | 2 -- .../mutation/cms/video_reaction_test.exs | 8 -------- test/mastani_server_web/query/cms/job_comment_test.exs | 3 --- test/mastani_server_web/query/cms/repo_comment_test.exs | 5 ----- test/mastani_server_web/query/cms/video_comment_test.exs | 5 ----- 11 files changed, 54 deletions(-) diff --git a/test/mastani_server/cms/job_comment_test.exs b/test/mastani_server/cms/job_comment_test.exs index 036a1592e..2839d9aa9 100644 --- a/test/mastani_server/cms/job_comment_test.exs +++ b/test/mastani_server/cms/job_comment_test.exs @@ -153,7 +153,6 @@ defmodule MastaniServer.Test.JobComment do end describe "[comment Reactions]" do - @tag :wip2 test "user can like a comment", ~m(comment user)a do {:ok, liked_comment} = CMS.like_comment(:job_comment, comment.id, user) @@ -162,14 +161,12 @@ defmodule MastaniServer.Test.JobComment do assert comment_preload.likes |> Enum.any?(&(&1.job_comment_id == comment.id)) end - @tag :wip test "user like comment twice fails", ~m(comment user)a do {:ok, _} = CMS.like_comment(:job_comment, comment.id, user) {:error, _error} = CMS.like_comment(:job_comment, comment.id, user) # TODO: fix err_msg later end - @tag :wip test "user can undo a like action", ~m(comment user)a do {:ok, like} = CMS.like_comment(:job_comment, comment.id, user) {:ok, _} = CMS.undo_like_comment(:job_comment, comment.id, user) @@ -178,7 +175,6 @@ defmodule MastaniServer.Test.JobComment do assert false == comment_preload.likes |> Enum.any?(&(&1.id == like.id)) end - @tag :wip test "user can dislike a comment", ~m(comment user)a do {:ok, disliked_comment} = CMS.dislike_comment(:job_comment, comment.id, user) @@ -187,7 +183,6 @@ defmodule MastaniServer.Test.JobComment do assert comment_preload.dislikes |> Enum.any?(&(&1.job_comment_id == comment.id)) end - @tag :wip test "user can undo a dislike action", ~m(comment user)a do {:ok, dislike} = CMS.dislike_comment(:job_comment, comment.id, user) {:ok, _} = CMS.undo_dislike_comment(:job_comment, comment.id, user) @@ -196,7 +191,6 @@ defmodule MastaniServer.Test.JobComment do assert false == comment_preload.dislikes |> Enum.any?(&(&1.id == dislike.id)) end - @tag :wip test "user can get paged likes of a job comment", ~m(comment)a do {:ok, user1} = db_insert(:user) {:ok, user2} = db_insert(:user) diff --git a/test/mastani_server/cms/video_reactions_test.exs b/test/mastani_server/cms/video_reactions_test.exs index f2cb3d345..6c746350f 100644 --- a/test/mastani_server/cms/video_reactions_test.exs +++ b/test/mastani_server/cms/video_reactions_test.exs @@ -13,7 +13,6 @@ defmodule MastaniServer.Test.VideoReactions do end describe "[cms video star/favorite reaction]" do - @tag :wip test "star and undo star reaction to video", ~m(user community video_attrs)a do {:ok, video} = CMS.create_content(community, :video, video_attrs, user) @@ -29,7 +28,6 @@ defmodule MastaniServer.Test.VideoReactions do assert 0 == reaction_users2 |> Enum.filter(fn ruser -> user.id == ruser.id end) |> length end - @tag :wip test "favorite and undo favorite reaction to video", ~m(user community video_attrs)a do {:ok, video} = CMS.create_content(community, :video, video_attrs, user) diff --git a/test/mastani_server_web/mutation/cms/job_comment_test.exs b/test/mastani_server_web/mutation/cms/job_comment_test.exs index 291c8d4e5..05894d92b 100644 --- a/test/mastani_server_web/mutation/cms/job_comment_test.exs +++ b/test/mastani_server_web/mutation/cms/job_comment_test.exs @@ -130,7 +130,6 @@ defmodule MastaniServer.Test.Mutation.JobComment do } } """ - @tag :wip test "login user can like a comment", ~m(user_conn comment)a do variables = %{thread: "JOB_COMMENT", id: comment.id} user_conn |> mutation_result(@like_comment_query, variables, "likeComment") @@ -147,7 +146,6 @@ defmodule MastaniServer.Test.Mutation.JobComment do } } """ - @tag :wip test "login user can undo a like action to comment", ~m(user comment)a do variables = %{thread: "JOB_COMMENT", id: comment.id} user_conn = simu_conn(:user, user) @@ -169,7 +167,6 @@ defmodule MastaniServer.Test.Mutation.JobComment do } } """ - @tag :wip test "login user can dislike a comment", ~m(user_conn comment)a do variables = %{thread: "JOB_COMMENT", id: comment.id} user_conn |> mutation_result(@dislike_comment_query, variables, "dislikeComment") @@ -186,7 +183,6 @@ defmodule MastaniServer.Test.Mutation.JobComment do } } """ - @tag :wip test "login user can undo dislike a comment", ~m(user comment)a do variables = %{thread: "JOB_COMMENT", id: comment.id} user_conn = simu_conn(:user, user) @@ -200,7 +196,6 @@ defmodule MastaniServer.Test.Mutation.JobComment do assert false == found.dislikes |> Enum.any?(&(&1.job_comment_id == comment.id)) end - @tag :wip test "unloged user do/undo like/dislike comment fails", ~m(guest_conn comment)a do variables = %{thread: "JOB_COMMENT", id: comment.id} diff --git a/test/mastani_server_web/mutation/cms/job_reaction_test.exs b/test/mastani_server_web/mutation/cms/job_reaction_test.exs index 8fa2e3600..6c5d2c373 100644 --- a/test/mastani_server_web/mutation/cms/job_reaction_test.exs +++ b/test/mastani_server_web/mutation/cms/job_reaction_test.exs @@ -21,7 +21,6 @@ defmodule MastaniServer.Test.Mutation.JobReaction do } } """ - @tag :wip test "login user can favorite a job", ~m(user_conn job)a do variables = %{id: job.id, thread: "JOB", action: "FAVORITE"} created = user_conn |> mutation_result(@query, variables, "reaction") @@ -29,7 +28,6 @@ defmodule MastaniServer.Test.Mutation.JobReaction do assert created["id"] == to_string(job.id) end - @tag :wip test "unauth user favorite a job fails", ~m(guest_conn job)a do variables = %{id: job.id, thread: "JOB", action: "FAVORITE"} @@ -44,7 +42,6 @@ defmodule MastaniServer.Test.Mutation.JobReaction do } } """ - @tag :wip test "login user can undo favorite a job", ~m(user_conn job user)a do {:ok, _} = CMS.reaction(:job, :favorite, job.id, user) @@ -54,7 +51,6 @@ defmodule MastaniServer.Test.Mutation.JobReaction do assert updated["id"] == to_string(job.id) end - @tag :wip test "unauth user undo favorite a job fails", ~m(guest_conn job)a do variables = %{id: job.id, thread: "JOB", action: "FAVORITE"} @@ -71,7 +67,6 @@ defmodule MastaniServer.Test.Mutation.JobReaction do } } """ - @tag :wip test "login user can star a job", ~m(user_conn job)a do variables = %{id: job.id, thread: "JOB", action: "STAR"} created = user_conn |> mutation_result(@query, variables, "reaction") @@ -79,7 +74,6 @@ defmodule MastaniServer.Test.Mutation.JobReaction do assert created["id"] == to_string(job.id) end - @tag :wip test "unauth user star a job fails", ~m(guest_conn job)a do variables = %{id: job.id, thread: "JOB", action: "STAR"} @@ -94,7 +88,6 @@ defmodule MastaniServer.Test.Mutation.JobReaction do } } """ - @tag :wip test "login user can undo star a job", ~m(user_conn job user)a do {:ok, _} = CMS.reaction(:job, :star, job.id, user) @@ -104,7 +97,6 @@ defmodule MastaniServer.Test.Mutation.JobReaction do assert updated["id"] == to_string(job.id) end - @tag :wip test "unauth user undo star a job fails", ~m(guest_conn job)a do variables = %{id: job.id, thread: "JOB", action: "STAR"} diff --git a/test/mastani_server_web/mutation/cms/post_reaction_test.exs b/test/mastani_server_web/mutation/cms/post_reaction_test.exs index a7943ab2c..b82cdcee4 100644 --- a/test/mastani_server_web/mutation/cms/post_reaction_test.exs +++ b/test/mastani_server_web/mutation/cms/post_reaction_test.exs @@ -21,7 +21,6 @@ defmodule MastaniServer.Test.Mutation.PostReaction do } } """ - @tag :wip test "login user can favorite a post", ~m(user_conn post)a do variables = %{id: post.id, thread: "POST", action: "FAVORITE"} created = user_conn |> mutation_result(@query, variables, "reaction") @@ -29,7 +28,6 @@ defmodule MastaniServer.Test.Mutation.PostReaction do assert created["id"] == to_string(post.id) end - @tag :wip test "unauth user favorite a post fails", ~m(guest_conn post)a do variables = %{id: post.id, thread: "POST", action: "FAVORITE"} @@ -44,7 +42,6 @@ defmodule MastaniServer.Test.Mutation.PostReaction do } } """ - @tag :wip test "login user can undo favorite a post", ~m(user_conn post user)a do {:ok, _} = CMS.reaction(:post, :favorite, post.id, user) @@ -54,7 +51,6 @@ defmodule MastaniServer.Test.Mutation.PostReaction do assert updated["id"] == to_string(post.id) end - @tag :wip test "unauth user undo favorite a post fails", ~m(guest_conn post)a do variables = %{id: post.id, thread: "POST", action: "FAVORITE"} @@ -71,7 +67,6 @@ defmodule MastaniServer.Test.Mutation.PostReaction do } } """ - @tag :wip test "login user can star a post", ~m(user_conn post)a do variables = %{id: post.id, thread: "POST", action: "STAR"} created = user_conn |> mutation_result(@query, variables, "reaction") @@ -79,7 +74,6 @@ defmodule MastaniServer.Test.Mutation.PostReaction do assert created["id"] == to_string(post.id) end - @tag :wip test "unauth user star a post fails", ~m(guest_conn post)a do variables = %{id: post.id, thread: "POST", action: "STAR"} @@ -94,7 +88,6 @@ defmodule MastaniServer.Test.Mutation.PostReaction do } } """ - @tag :wip test "login user can undo star a post", ~m(user_conn post user)a do {:ok, _} = CMS.reaction(:post, :star, post.id, user) @@ -104,7 +97,6 @@ defmodule MastaniServer.Test.Mutation.PostReaction do assert updated["id"] == to_string(post.id) end - @tag :wip test "unauth user undo star a post fails", ~m(guest_conn post)a do variables = %{id: post.id, thread: "POST", action: "STAR"} diff --git a/test/mastani_server_web/mutation/cms/repo_comment_test.exs b/test/mastani_server_web/mutation/cms/repo_comment_test.exs index 6e941ede3..d0e9439d3 100644 --- a/test/mastani_server_web/mutation/cms/repo_comment_test.exs +++ b/test/mastani_server_web/mutation/cms/repo_comment_test.exs @@ -49,7 +49,6 @@ defmodule MastaniServer.Test.Mutation.RepoComment do } } """ - @tag :wip test "comment owner can delete comment", ~m(user repo)a do variables = %{thread: "REPO", id: repo.id, body: "this a comment"} @@ -62,7 +61,6 @@ defmodule MastaniServer.Test.Mutation.RepoComment do assert deleted["id"] == created["id"] end - @tag :wip test "unauth user delete comment fails", ~m(user_conn guest_conn repo)a do variables = %{thread: "REPO", id: repo.id, body: "this a comment"} {:ok, owner} = db_insert(:user) diff --git a/test/mastani_server_web/mutation/cms/video_comment_test.exs b/test/mastani_server_web/mutation/cms/video_comment_test.exs index fc9de2177..88eda6787 100644 --- a/test/mastani_server_web/mutation/cms/video_comment_test.exs +++ b/test/mastani_server_web/mutation/cms/video_comment_test.exs @@ -49,7 +49,6 @@ defmodule MastaniServer.Test.Mutation.VideoComment do } } """ - @tag :wip test "comment owner can delete comment", ~m(user video)a do variables = %{thread: "VIDEO", id: video.id, body: "this a comment"} @@ -62,7 +61,6 @@ defmodule MastaniServer.Test.Mutation.VideoComment do assert deleted["id"] == created["id"] end - @tag :wip test "unauth user delete comment fails", ~m(user_conn guest_conn video)a do variables = %{thread: "VIDEO", id: video.id, body: "this a comment"} {:ok, owner} = db_insert(:user) diff --git a/test/mastani_server_web/mutation/cms/video_reaction_test.exs b/test/mastani_server_web/mutation/cms/video_reaction_test.exs index b4180f239..fe20816c4 100644 --- a/test/mastani_server_web/mutation/cms/video_reaction_test.exs +++ b/test/mastani_server_web/mutation/cms/video_reaction_test.exs @@ -21,7 +21,6 @@ defmodule MastaniServer.Test.Mutation.VideoReaction do } } """ - @tag :wip test "login user can favorite a video", ~m(user_conn video)a do variables = %{id: video.id, thread: "VIDEO", action: "FAVORITE"} created = user_conn |> mutation_result(@query, variables, "reaction") @@ -29,7 +28,6 @@ defmodule MastaniServer.Test.Mutation.VideoReaction do assert created["id"] == to_string(video.id) end - @tag :wip test "unauth user favorite a video fails", ~m(guest_conn video)a do variables = %{id: video.id, thread: "VIDEO", action: "FAVORITE"} @@ -44,7 +42,6 @@ defmodule MastaniServer.Test.Mutation.VideoReaction do } } """ - @tag :wip test "login user can undo favorite a video", ~m(user_conn video user)a do {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) @@ -54,7 +51,6 @@ defmodule MastaniServer.Test.Mutation.VideoReaction do assert updated["id"] == to_string(video.id) end - @tag :wip test "unauth user undo favorite a video fails", ~m(guest_conn video)a do variables = %{id: video.id, thread: "VIDEO", action: "FAVORITE"} @@ -71,7 +67,6 @@ defmodule MastaniServer.Test.Mutation.VideoReaction do } } """ - @tag :wip test "login user can star a video", ~m(user_conn video)a do variables = %{id: video.id, thread: "VIDEO", action: "STAR"} created = user_conn |> mutation_result(@query, variables, "reaction") @@ -79,7 +74,6 @@ defmodule MastaniServer.Test.Mutation.VideoReaction do assert created["id"] == to_string(video.id) end - @tag :wip test "unauth user star a video fails", ~m(guest_conn video)a do variables = %{id: video.id, thread: "VIDEO", action: "STAR"} @@ -94,7 +88,6 @@ defmodule MastaniServer.Test.Mutation.VideoReaction do } } """ - @tag :wip test "login user can undo star a video", ~m(user_conn video user)a do {:ok, _} = CMS.reaction(:video, :star, video.id, user) @@ -104,7 +97,6 @@ defmodule MastaniServer.Test.Mutation.VideoReaction do assert updated["id"] == to_string(video.id) end - @tag :wip test "unauth user undo star a video fails", ~m(guest_conn video)a do variables = %{id: video.id, thread: "VIDEO", action: "STAR"} diff --git a/test/mastani_server_web/query/cms/job_comment_test.exs b/test/mastani_server_web/query/cms/job_comment_test.exs index 0091de0dd..06aee3a6f 100644 --- a/test/mastani_server_web/query/cms/job_comment_test.exs +++ b/test/mastani_server_web/query/cms/job_comment_test.exs @@ -47,7 +47,6 @@ defmodule MastaniServer.Test.Query.JobComment do assert results["totalCount"] == 30 end - @tag :wip test "MOST_LIKES filter should work", ~m(guest_conn job user)a do body = "test comment" @@ -91,7 +90,6 @@ defmodule MastaniServer.Test.Query.JobComment do assert entries |> Enum.at(1) |> Map.get("likesCount") == 4 end - @tag :wip test "MOST_DISLIKES filter should work", ~m(guest_conn job user)a do body = "test comment" @@ -143,7 +141,6 @@ defmodule MastaniServer.Test.Query.JobComment do } } """ - @tag :wip test "login user can get hasLiked feedBack", ~m(user_conn job user)a do body = "test comment" diff --git a/test/mastani_server_web/query/cms/repo_comment_test.exs b/test/mastani_server_web/query/cms/repo_comment_test.exs index 434965fcb..798b902de 100644 --- a/test/mastani_server_web/query/cms/repo_comment_test.exs +++ b/test/mastani_server_web/query/cms/repo_comment_test.exs @@ -31,7 +31,6 @@ defmodule MastaniServer.Test.Query.RepoComment do } } """ - @tag :wip test "guest user can get a paged comment", ~m(guest_conn repo user)a do body = "test comment" @@ -48,7 +47,6 @@ defmodule MastaniServer.Test.Query.RepoComment do assert results["totalCount"] == 30 end - @tag :wip2 test "MOST_LIKES filter should work", ~m(guest_conn repo user)a do body = "test comment" @@ -92,7 +90,6 @@ defmodule MastaniServer.Test.Query.RepoComment do assert entries |> Enum.at(1) |> Map.get("likesCount") == 4 end - @tag :wip test "MOST_DISLIKES filter should work", ~m(guest_conn repo user)a do body = "test comment" @@ -144,7 +141,6 @@ defmodule MastaniServer.Test.Query.RepoComment do } } """ - @tag :wip test "login user can get hasLiked feedBack", ~m(user_conn repo user)a do body = "test comment" @@ -188,7 +184,6 @@ defmodule MastaniServer.Test.Query.RepoComment do } } """ - @tag :wip test "guest user can get replies info", ~m(guest_conn repo user)a do body = "test comment" {:ok, comment} = CMS.create_comment(:repo, repo.id, body, user) diff --git a/test/mastani_server_web/query/cms/video_comment_test.exs b/test/mastani_server_web/query/cms/video_comment_test.exs index 5fc1e36d6..ddb72697d 100644 --- a/test/mastani_server_web/query/cms/video_comment_test.exs +++ b/test/mastani_server_web/query/cms/video_comment_test.exs @@ -31,7 +31,6 @@ defmodule MastaniServer.Test.Query.VideoComment do } } """ - @tag :wip test "guest user can get a paged comment", ~m(guest_conn video user)a do body = "test comment" @@ -48,7 +47,6 @@ defmodule MastaniServer.Test.Query.VideoComment do assert results["totalCount"] == 30 end - @tag :wip2 test "MOST_LIKES filter should work", ~m(guest_conn video user)a do body = "test comment" @@ -92,7 +90,6 @@ defmodule MastaniServer.Test.Query.VideoComment do assert entries |> Enum.at(1) |> Map.get("likesCount") == 4 end - @tag :wip test "MOST_DISLIKES filter should work", ~m(guest_conn video user)a do body = "test comment" @@ -144,7 +141,6 @@ defmodule MastaniServer.Test.Query.VideoComment do } } """ - @tag :wip test "login user can get hasLiked feedBack", ~m(user_conn video user)a do body = "test comment" @@ -188,7 +184,6 @@ defmodule MastaniServer.Test.Query.VideoComment do } } """ - @tag :wip test "guest user can get replies info", ~m(guest_conn video user)a do body = "test comment" {:ok, comment} = CMS.create_comment(:video, video.id, body, user) From e4fa80d451c2833f8b2e97b169005ae20f94338d Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 9 Oct 2018 13:22:28 +0800 Subject: [PATCH 049/129] refactor(reaction users): more tests on paged reaction users --- lib/mastani_server_web/schema/cms/cms_misc.ex | 12 +- .../schema/cms/cms_queries.ex | 10 +- .../schema/cms/mutations/operation.ex | 8 +- .../query/cms/reaction_users_test.exs | 117 ++++++++++++++++++ 4 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 test/mastani_server_web/query/cms/reaction_users_test.exs diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index 1e9ba94b5..3b044b5b6 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -17,7 +17,7 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do enum(:count_type, do: value(:count)) enum(:viewer_did_type, do: value(:viewer_did)) - enum(:favorite_action, do: value(:favorite)) + # enum(:favorite_action, do: value(:favorite)) enum(:star_action, do: value(:star)) enum(:comment_action, do: value(:comment)) @@ -27,10 +27,16 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do value(false) end - enum :cms_action do + enum :react_action do value(:favorite) value(:star) - value(:watch) + # value(:watch) + end + + enum :react_thread do + value(:post) + value(:job) + value(:video) end enum :cms_comment do diff --git a/lib/mastani_server_web/schema/cms/cms_queries.ex b/lib/mastani_server_web/schema/cms/cms_queries.ex index b7a7f4254..d43004ce6 100644 --- a/lib/mastani_server_web/schema/cms/cms_queries.ex +++ b/lib/mastani_server_web/schema/cms/cms_queries.ex @@ -132,11 +132,13 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do resolve(&R.CMS.paged_jobs/3) end - field :favorite_users, :paged_users do + @desc "get paged users of a reaction" + # field :favorite_users, :paged_users do + field :reaction_users, :paged_users do arg(:id, non_null(:id)) - arg(:thread, :cms_thread, default_value: :post) - arg(:action, :favorite_action, default_value: :favorite) - arg(:filter, :paged_article_filter) + arg(:thread, :react_thread, default_value: :post) + arg(:action, non_null(:react_action)) + arg(:filter, non_null(:paged_filter)) middleware(M.PageSizeProof) resolve(&R.CMS.reaction_users/3) diff --git a/lib/mastani_server_web/schema/cms/mutations/operation.ex b/lib/mastani_server_web/schema/cms/mutations/operation.ex index 06a888a9d..1e1e6ffc7 100644 --- a/lib/mastani_server_web/schema/cms/mutations/operation.ex +++ b/lib/mastani_server_web/schema/cms/mutations/operation.ex @@ -131,8 +131,8 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Operation do field :reaction, :article do arg(:id, non_null(:id)) - arg(:thread, non_null(:cms_thread)) - arg(:action, non_null(:cms_action)) + arg(:thread, non_null(:react_thread)) + arg(:action, non_null(:react_action)) middleware(M.Authorize, :login) resolve(&R.CMS.reaction/3) @@ -140,8 +140,8 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Operation do field :undo_reaction, :article do arg(:id, non_null(:id)) - arg(:thread, non_null(:cms_thread)) - arg(:action, non_null(:cms_action)) + arg(:thread, non_null(:react_thread)) + arg(:action, non_null(:react_action)) middleware(M.Authorize, :login) resolve(&R.CMS.undo_reaction/3) diff --git a/test/mastani_server_web/query/cms/reaction_users_test.exs b/test/mastani_server_web/query/cms/reaction_users_test.exs new file mode 100644 index 000000000..68c043233 --- /dev/null +++ b/test/mastani_server_web/query/cms/reaction_users_test.exs @@ -0,0 +1,117 @@ +defmodule MastaniServer.Test.Query.ReactionUsers do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, post} = db_insert(:post) + {:ok, job} = db_insert(:job) + {:ok, video} = db_insert(:video) + {:ok, user} = db_insert(:user) + {:ok, user2} = db_insert(:user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(user_conn guest_conn user user2 post job video)a} + end + + @query """ + query($id: ID!, $thread: ReactThread, $action: ReactAction!, $filter: PagedFilter!) { + reactionUsers(id: $id, thread: $thread, action: $action, filter: $filter) { + entries { + id + avatar + nickname + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + describe "[favrotes users]" do + @tag :wip + test "guest can get favroted user list after favrote to a post", + ~m(guest_conn post user user2)a do + {:ok, _} = CMS.reaction(:post, :favorite, post.id, user) + {:ok, _} = CMS.reaction(:post, :favorite, post.id, user2) + + variables = %{id: post.id, thread: "POST", action: "FAVORITE", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "reactionUsers") + + assert results |> is_valid_pagination? + assert results["totalCount"] == 2 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user2.id))) + end + + @tag :wip + test "guest can get favroted user list after favrote to a job", + ~m(guest_conn job user user2)a do + {:ok, _} = CMS.reaction(:job, :favorite, job.id, user) + {:ok, _} = CMS.reaction(:job, :favorite, job.id, user2) + + variables = %{id: job.id, thread: "JOB", action: "FAVORITE", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "reactionUsers") + + assert results |> is_valid_pagination? + assert results["totalCount"] == 2 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user2.id))) + end + + @tag :wip + test "guest can get favroted user list after favrote to a video", + ~m(guest_conn video user user2)a do + {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) + {:ok, _} = CMS.reaction(:video, :favorite, video.id, user2) + + variables = %{ + id: video.id, + thread: "VIDEO", + action: "FAVORITE", + filter: %{page: 1, size: 20} + } + + results = guest_conn |> query_result(@query, variables, "reactionUsers") + + assert results |> is_valid_pagination? + assert results["totalCount"] == 2 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user2.id))) + end + end + + describe "[stars users]" do + @tag :wip + test "guest can get stared user list after star to a post", ~m(guest_conn post user user2)a do + {:ok, _} = CMS.reaction(:post, :star, post.id, user) + {:ok, _} = CMS.reaction(:post, :star, post.id, user2) + + variables = %{id: post.id, thread: "POST", action: "STAR", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "reactionUsers") + + assert results |> is_valid_pagination? + assert results["totalCount"] == 2 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user2.id))) + end + + @tag :wip + test "guest can get stared user list after star to a video", + ~m(guest_conn video user user2)a do + {:ok, _} = CMS.reaction(:video, :star, video.id, user) + {:ok, _} = CMS.reaction(:video, :star, video.id, user2) + + variables = %{id: video.id, thread: "VIDEO", action: "STAR", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "reactionUsers") + + assert results |> is_valid_pagination? + assert results["totalCount"] == 2 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user2.id))) + end + end +end From 468b8c104be5eae6f6593fab364f10cf919b89d1 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 9 Oct 2018 13:26:10 +0800 Subject: [PATCH 050/129] chore(clean up): remove @wip & add @doc --- lib/mastani_server_web/schema/cms/cms_queries.ex | 4 ++-- lib/mastani_server_web/schema/cms/mutations/operation.ex | 2 ++ test/mastani_server_web/query/cms/reaction_users_test.exs | 5 ----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/mastani_server_web/schema/cms/cms_queries.ex b/lib/mastani_server_web/schema/cms/cms_queries.ex index d43004ce6..67c87ba58 100644 --- a/lib/mastani_server_web/schema/cms/cms_queries.ex +++ b/lib/mastani_server_web/schema/cms/cms_queries.ex @@ -132,8 +132,7 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do resolve(&R.CMS.paged_jobs/3) end - @desc "get paged users of a reaction" - # field :favorite_users, :paged_users do + @desc "get paged users of a reaction related to cms content" field :reaction_users, :paged_users do arg(:id, non_null(:id)) arg(:thread, :react_thread, default_value: :post) @@ -145,6 +144,7 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do end # get all tags + @desc "get paged tags" field :paged_tags, :paged_tags do arg(:filter, non_null(:paged_filter)) diff --git a/lib/mastani_server_web/schema/cms/mutations/operation.ex b/lib/mastani_server_web/schema/cms/mutations/operation.ex index 1e1e6ffc7..aa3f61ea7 100644 --- a/lib/mastani_server_web/schema/cms/mutations/operation.ex +++ b/lib/mastani_server_web/schema/cms/mutations/operation.ex @@ -129,6 +129,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Operation do resolve(&R.CMS.unset_community/3) end + @desc "react on a cms content" field :reaction, :article do arg(:id, non_null(:id)) arg(:thread, non_null(:react_thread)) @@ -138,6 +139,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Operation do resolve(&R.CMS.reaction/3) end + @desc "undoreact on a cms content" field :undo_reaction, :article do arg(:id, non_null(:id)) arg(:thread, non_null(:react_thread)) diff --git a/test/mastani_server_web/query/cms/reaction_users_test.exs b/test/mastani_server_web/query/cms/reaction_users_test.exs index 68c043233..be2740c54 100644 --- a/test/mastani_server_web/query/cms/reaction_users_test.exs +++ b/test/mastani_server_web/query/cms/reaction_users_test.exs @@ -32,7 +32,6 @@ defmodule MastaniServer.Test.Query.ReactionUsers do } """ describe "[favrotes users]" do - @tag :wip test "guest can get favroted user list after favrote to a post", ~m(guest_conn post user user2)a do {:ok, _} = CMS.reaction(:post, :favorite, post.id, user) @@ -47,7 +46,6 @@ defmodule MastaniServer.Test.Query.ReactionUsers do assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user2.id))) end - @tag :wip test "guest can get favroted user list after favrote to a job", ~m(guest_conn job user user2)a do {:ok, _} = CMS.reaction(:job, :favorite, job.id, user) @@ -62,7 +60,6 @@ defmodule MastaniServer.Test.Query.ReactionUsers do assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user2.id))) end - @tag :wip test "guest can get favroted user list after favrote to a video", ~m(guest_conn video user user2)a do {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) @@ -85,7 +82,6 @@ defmodule MastaniServer.Test.Query.ReactionUsers do end describe "[stars users]" do - @tag :wip test "guest can get stared user list after star to a post", ~m(guest_conn post user user2)a do {:ok, _} = CMS.reaction(:post, :star, post.id, user) {:ok, _} = CMS.reaction(:post, :star, post.id, user2) @@ -99,7 +95,6 @@ defmodule MastaniServer.Test.Query.ReactionUsers do assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user2.id))) end - @tag :wip test "guest can get stared user list after star to a video", ~m(guest_conn video user user2)a do {:ok, _} = CMS.reaction(:video, :star, video.id, user) From 6cbc9cea2b1c76b8275dc12b6602a5885043c62f Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 10 Oct 2018 12:28:23 +0800 Subject: [PATCH 051/129] fix: followersCount makes right by rm dataloader --- lib/mastani_server/accounts/accounts.ex | 1 + lib/mastani_server/accounts/delegates/fans.ex | 7 +++++++ lib/mastani_server/accounts/utils/loader.ex | 7 ++++++- .../resolvers/accounts_resolver.ex | 2 ++ .../schema/account/account_types.ex | 18 +++++++++++++---- .../query/accounts/achievement_test.exs | 20 ++++++++++++------- .../query/accounts/fans_test.exs | 15 ++++++++++---- 7 files changed, 54 insertions(+), 16 deletions(-) diff --git a/lib/mastani_server/accounts/accounts.ex b/lib/mastani_server/accounts/accounts.ex index a996257a9..9951069d0 100644 --- a/lib/mastani_server/accounts/accounts.ex +++ b/lib/mastani_server/accounts/accounts.ex @@ -35,6 +35,7 @@ defmodule MastaniServer.Accounts do defdelegate undo_follow(user, follower), to: Fans defdelegate fetch_followers(user, filter), to: Fans defdelegate fetch_followings(user, filter), to: Fans + defdelegate count_followers(user), to: Fans # reacted contents defdelegate reacted_contents(thread, react, filter, user), to: ReactedContents diff --git a/lib/mastani_server/accounts/delegates/fans.ex b/lib/mastani_server/accounts/delegates/fans.ex index 866017574..96220998f 100644 --- a/lib/mastani_server/accounts/delegates/fans.ex +++ b/lib/mastani_server/accounts/delegates/fans.ex @@ -105,6 +105,13 @@ defmodule MastaniServer.Accounts.Delegate.Fans do {:error, [message: "follow acieve fails", code: ecode(:react_fails)]} end + def count_followers(%User{id: user_id}) do + UserFollower + |> where([uf], uf.follower_id == ^user_id) + |> ORM.count() + |> done() + end + @doc """ get paged followers of a user """ diff --git a/lib/mastani_server/accounts/utils/loader.ex b/lib/mastani_server/accounts/utils/loader.ex index 83d5a02e6..b225d0d7a 100644 --- a/lib/mastani_server/accounts/utils/loader.ex +++ b/lib/mastani_server/accounts/utils/loader.ex @@ -24,10 +24,15 @@ defmodule MastaniServer.Accounts.Utils.Loader do |> select([u, c], c) end + # TODO: fix later, this is not working def query({"users_followers", UserFollower}, %{count: _}) do + # UserFollower + # |> group_by([f], f.user_id) + # |> select([f], count(f.id)) + UserFollower |> group_by([f], f.user_id) - |> select([f], count(f.id)) + |> select([f], count(f.follower_id)) end def query({"users_followings", UserFollowing}, %{count: _}) do diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index d8d9de326..23c06a5dd 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -76,6 +76,8 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.undo_follow(cur_user, %User{id: user_id}) end + def count_followers(root, _args, _info), do: Accounts.count_followers(%User{id: root.id}) + def paged_followers(_root, ~m(user_id filter)a, _info) do Accounts.fetch_followers(%User{id: user_id}, filter) end diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index a9865e0e0..155bee8f7 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -77,13 +77,22 @@ defmodule MastaniServerWeb.Schema.Account.Types do middleware(M.ConvertToInt) end + # NOTE: dataloader not work at this case + # field :followers_count, :integer do + # arg(:count, :count_type, default_value: :count) + + # resolve(dataloader(Accounts, :followers)) + # middleware(M.ConvertToInt) + # end + + @doc "get follower users count" field :followers_count, :integer do arg(:count, :count_type, default_value: :count) - resolve(dataloader(Accounts, :followers)) - middleware(M.ConvertToInt) + resolve(&R.Accounts.count_followers/3) end + @doc "get following users count" field :followings_count, :integer do arg(:count, :count_type, default_value: :count) @@ -91,6 +100,7 @@ defmodule MastaniServerWeb.Schema.Account.Types do middleware(M.ConvertToInt) end + @doc "wether viewer has followed" field :viewer_has_followed, :boolean do arg(:viewer_did, :viewer_did_type, default_value: :viewer_did) @@ -203,10 +213,10 @@ defmodule MastaniServerWeb.Schema.Account.Types do object :achievement do field(:reputation, :integer) - field(:followers_count, :integer) + # field(:followers_count, :integer) field(:contents_stared_count, :integer) field(:contents_favorited_count, :integer) - field(:contents_watched_count, :integer) + # field(:contents_watched_count, :integer) end object :token_info do diff --git a/test/mastani_server_web/query/accounts/achievement_test.exs b/test/mastani_server_web/query/accounts/achievement_test.exs index 2e8b5ef43..f5e87c6d6 100644 --- a/test/mastani_server_web/query/accounts/achievement_test.exs +++ b/test/mastani_server_web/query/accounts/achievement_test.exs @@ -22,14 +22,15 @@ defmodule MastaniServer.Test.Query.Account.Achievement do query($id: ID!) { user(id: $id) { id + followersCount + followingsCount achievement { reputation - followersCount } } } """ - test "new you has no acheiveements", ~m(guest_conn user)a do + test "new user has no acheiveements", ~m(guest_conn user)a do variables = %{id: user.id} results = guest_conn |> query_result(@query, variables, "user") @@ -38,13 +39,19 @@ defmodule MastaniServer.Test.Query.Account.Achievement do test "inc user's achievement after user got followed", ~m(guest_conn user)a do {:ok, user2} = db_insert(:user) + {:ok, user3} = db_insert(:user) + {:ok, user4} = db_insert(:user) + user2 |> Accounts.follow(user) + user |> Accounts.follow(user2) + user3 |> Accounts.follow(user2) + user3 |> Accounts.follow(user4) - variables = %{id: user.id} + variables = %{id: user2.id} results = guest_conn |> query_result(@query, variables, "user") - assert results["achievement"] |> Map.get("followersCount") == @follow_weight - assert results["achievement"] |> Map.get("reputation") == @follow_weight + assert results |> Map.get("followersCount") == 2 + assert results["achievement"] |> Map.get("reputation") == 2 * @follow_weight end test "minus user's achievement after user get cancle followed", ~m(guest_conn user)a do @@ -61,8 +68,7 @@ defmodule MastaniServer.Test.Query.Account.Achievement do variables = %{id: user.id} results = guest_conn |> query_result(@query, variables, "user") - assert results["achievement"] |> Map.get("followersCount") == - @follow_weight * (total_count - 1) + assert results |> Map.get("followersCount") == total_count - 1 assert results["achievement"] |> Map.get("reputation") == @follow_weight * (total_count - 1) end diff --git a/test/mastani_server_web/query/accounts/fans_test.exs b/test/mastani_server_web/query/accounts/fans_test.exs index 610098b8d..be7b77915 100644 --- a/test/mastani_server_web/query/accounts/fans_test.exs +++ b/test/mastani_server_web/query/accounts/fans_test.exs @@ -26,12 +26,14 @@ defmodule MastaniServer.Test.Query.Account.Fans do variables = %{filter: %{page: 1, size: 20}} {:ok, user2} = db_insert(:user) + {:ok, user3} = db_insert(:user) {:ok, _followeer} = user |> Accounts.follow(user2) + {:ok, _followeer} = user3 |> Accounts.follow(user2) user2_conn = simu_conn(:user, user2) results = user2_conn |> query_result(@query, variables, "pagedFollowers") - assert results |> Map.get("totalCount") == 1 + assert results |> Map.get("totalCount") == 2 assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user.id))) end @@ -61,11 +63,15 @@ defmodule MastaniServer.Test.Query.Account.Fans do variables = %{filter: %{page: 1, size: 20}} {:ok, user2} = db_insert(:user) + {:ok, user3} = db_insert(:user) + {:ok, user4} = db_insert(:user) {:ok, _followeer} = user |> Accounts.follow(user2) + {:ok, _followeer} = user |> Accounts.follow(user3) + {:ok, _followeer} = user |> Accounts.follow(user4) results = user_conn |> query_result(@query, variables, "pagedFollowings") - assert results |> Map.get("totalCount") == 1 + assert results |> Map.get("totalCount") == 3 assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(user2.id))) end @@ -92,12 +98,13 @@ defmodule MastaniServer.Test.Query.Account.Fans do total_count = 15 {:ok, users} = db_insert_multi(:user, total_count) - Enum.each(users, fn cool_user -> - {:ok, _} = user |> Accounts.follow(cool_user) + Enum.each(users, fn other_user -> + {:ok, _} = other_user |> Accounts.follow(user) end) variables = %{id: user.id} resolts = user_conn |> query_result(@query, variables, "user") + assert resolts |> Map.get("followersCount") == total_count end From 199a4470d0b77ec26281c906b813c4899c001050 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 10 Oct 2018 12:28:48 +0800 Subject: [PATCH 052/129] style: fmt --- .../query/cms/reaction_users_test.exs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/mastani_server_web/query/cms/reaction_users_test.exs b/test/mastani_server_web/query/cms/reaction_users_test.exs index be2740c54..1ddf59d23 100644 --- a/test/mastani_server_web/query/cms/reaction_users_test.exs +++ b/test/mastani_server_web/query/cms/reaction_users_test.exs @@ -17,12 +17,17 @@ defmodule MastaniServer.Test.Query.ReactionUsers do end @query """ - query($id: ID!, $thread: ReactThread, $action: ReactAction!, $filter: PagedFilter!) { + query( + $id: ID! + $thread: ReactThread + $action: ReactAction! + $filter: PagedFilter! + ) { reactionUsers(id: $id, thread: $thread, action: $action, filter: $filter) { entries { - id - avatar - nickname + id + avatar + nickname } totalPages totalCount From ef714663d1fcca51fea4d9c2be813344730e2f57 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 11 Oct 2018 22:49:37 +0800 Subject: [PATCH 053/129] refactor(user favrites cats): enhance support for jobs / videos --- lib/mastani_server/accounts/accounts.ex | 1 + .../accounts/delegates/favorite_category.ex | 43 +++--- .../accounts/delegates/reacted_contents.ex | 20 +++ .../accounts/favorite_category.ex | 2 + lib/mastani_server/accounts/user.ex | 1 + lib/mastani_server/accounts/utils/loader.ex | 10 +- lib/mastani_server/cms/job_favorite.ex | 5 +- lib/mastani_server/cms/post_favorite.ex | 2 +- lib/mastani_server/cms/video_favorite.ex | 5 +- .../resolvers/accounts_resolver.ex | 17 +-- .../schema/account/account_queries.ex | 21 ++- .../schema/account/account_types.ex | 28 +++- ...20181011131417_add_category_id_to_jobs.exs | 11 ++ ...181011133216_add_category_id_to_videos.exs | 11 ++ ...add_category_index_to_posts_favoritess.exs | 7 + .../accounts/favorite_category_test.exs | 11 +- .../query/accounts/favorite_category_test.exs | 8 +- .../query/accounts/favorited_jobs_test.exs | 40 +++++- .../query/accounts/favorited_posts_test.exs | 40 +++++- .../query/accounts/favorited_videos_test.exs | 123 ++++++++++++++++++ 20 files changed, 354 insertions(+), 52 deletions(-) create mode 100644 priv/repo/migrations/20181011131417_add_category_id_to_jobs.exs create mode 100644 priv/repo/migrations/20181011133216_add_category_id_to_videos.exs create mode 100644 priv/repo/migrations/20181011133312_add_category_index_to_posts_favoritess.exs create mode 100644 test/mastani_server_web/query/accounts/favorited_videos_test.exs diff --git a/lib/mastani_server/accounts/accounts.ex b/lib/mastani_server/accounts/accounts.ex index 9951069d0..8696c4d2e 100644 --- a/lib/mastani_server/accounts/accounts.ex +++ b/lib/mastani_server/accounts/accounts.ex @@ -39,6 +39,7 @@ defmodule MastaniServer.Accounts do # reacted contents defdelegate reacted_contents(thread, react, filter, user), to: ReactedContents + defdelegate reacted_contents(thread, react, category_id, filter, user), to: ReactedContents # mentions defdelegate fetch_mentions(user, filter), to: Mails diff --git a/lib/mastani_server/accounts/delegates/favorite_category.ex b/lib/mastani_server/accounts/delegates/favorite_category.ex index 1fcda8639..132c13659 100644 --- a/lib/mastani_server/accounts/delegates/favorite_category.ex +++ b/lib/mastani_server/accounts/delegates/favorite_category.ex @@ -83,35 +83,22 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do |> done() end - # def update_favorie_category(), do ... - - # def delete_favorie_category(), do ... - - alias CMS.PostFavorite - - defp check_dup_category(content, category) do - case content.category_id !== category.id do - true -> {:ok, ""} - false -> {:error, [message: "viewer has already categoried", code: ecode(:already_did)]} - end - end - - defp content_favorite_result(:post, content_id, user_id) do - PostFavorite |> ORM.find_by(%{post_id: content_id, user_id: user_id}) - end - + alias CMS.{PostFavorite, JobFavorite, VideoFavorite} + @doc """ + set category for favorited content (post, job, video ...) + """ def set_favorites(%User{} = user, thread, content_id, category_id) do with {:ok, favorite_category} <- FavoriteCategory |> ORM.find_by(%{user_id: user.id, id: category_id}) do Multi.new() |> Multi.run(:favorite_content, fn _ -> - case content_favorite_result(thread, content_id, user.id) do + case find_content_favorite(thread, content_id, user.id) do {:ok, content_favorite} -> check_dup_category(content_favorite, favorite_category) {:error, _} -> CMS.reaction(thread, :favorite, content_id, user) end end) |> Multi.run(:update_category_id, fn _ -> - {:ok, content_favorite} = content_favorite_result(thread, content_id, user.id) + {:ok, content_favorite} = find_content_favorite(thread, content_id, user.id) content_favorite |> ORM.update(%{category_id: favorite_category.id}) end) @@ -143,7 +130,7 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do FavoriteCategory |> ORM.find_by(%{user_id: user.id, id: category_id}) do Multi.new() |> Multi.run(:remove_favorite_record, fn _ -> - {:ok, content_favorite} = content_favorite_result(thread, content_id, user.id) + {:ok, content_favorite} = find_content_favorite(thread, content_id, user.id) content_favorite |> ORM.delete() end) @@ -166,4 +153,20 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do defp unset_favorites_result({:error, :dec_count, result, _steps}) do {:error, result} end + + defp find_content_favorite(:post, content_id, user_id), + do: PostFavorite |> ORM.find_by(%{post_id: content_id, user_id: user_id}) + + defp find_content_favorite(:job, content_id, user_id), + do: JobFavorite |> ORM.find_by(%{job_id: content_id, user_id: user_id}) + + defp find_content_favorite(:video, content_id, user_id), + do: VideoFavorite |> ORM.find_by(%{video_id: content_id, user_id: user_id}) + + defp check_dup_category(content, category) do + case content.category_id !== category.id do + true -> {:ok, ""} + false -> {:error, [message: "viewer has already categoried", code: ecode(:already_did)]} + end + end end diff --git a/lib/mastani_server/accounts/delegates/reacted_contents.ex b/lib/mastani_server/accounts/delegates/reacted_contents.ex index 49516e7b9..adf5200b1 100644 --- a/lib/mastani_server/accounts/delegates/reacted_contents.ex +++ b/lib/mastani_server/accounts/delegates/reacted_contents.ex @@ -10,6 +10,26 @@ defmodule MastaniServer.Accounts.Delegate.ReactedContents do alias Helper.{ORM, QueryBuilder} alias MastaniServer.Accounts.User + @doc """ + paged favorite contents of a spec category + """ + def reacted_contents(thread, :favorite, category_id, ~m(page size)a = filter, %User{id: user_id}) do + with {:ok, action} <- match_action(thread, :favorite) do + action.reactor + |> where([f], f.user_id == ^user_id) + |> join(:inner, [f], p in assoc(f, ^thread)) + |> join(:inner, [f], c in assoc(f, :category)) + |> where([f, p, c], c.id == ^category_id) + |> select([f, p], p) + |> QueryBuilder.filter_pack(filter) + |> ORM.paginater(~m(page size)a) + |> done() + end + end + + @doc """ + paged favorite contents + """ def reacted_contents(thread, react, ~m(page size)a = filter, %User{id: user_id}) do with {:ok, action} <- match_action(thread, react) do action.reactor diff --git a/lib/mastani_server/accounts/favorite_category.ex b/lib/mastani_server/accounts/favorite_category.ex index 25bd35bac..96133c891 100644 --- a/lib/mastani_server/accounts/favorite_category.ex +++ b/lib/mastani_server/accounts/favorite_category.ex @@ -19,6 +19,8 @@ defmodule MastaniServer.Accounts.FavoriteCategory do field(:index, :integer) field(:total_count, :integer, default: 0) field(:private, :boolean, default: false) + # TODO: + # last_updated timestamps(type: :utc_datetime) end diff --git a/lib/mastani_server/accounts/user.ex b/lib/mastani_server/accounts/user.ex index 6d24e1dad..335a1460e 100644 --- a/lib/mastani_server/accounts/user.ex +++ b/lib/mastani_server/accounts/user.ex @@ -51,6 +51,7 @@ defmodule MastaniServer.Accounts.User do has_many(:subscribed_communities, {"communities_subscribers", CMS.CommunitySubscriber}) has_many(:favorited_posts, {"posts_favorites", CMS.PostFavorite}) has_many(:favorited_jobs, {"jobs_favorites", CMS.JobFavorite}) + has_many(:favorited_videos, {"videos_favorites", CMS.VideoFavorite}) has_many(:favorite_categories, {"favorite_categories", FavoriteCategory}) diff --git a/lib/mastani_server/accounts/utils/loader.ex b/lib/mastani_server/accounts/utils/loader.ex index b225d0d7a..13c7c1022 100644 --- a/lib/mastani_server/accounts/utils/loader.ex +++ b/lib/mastani_server/accounts/utils/loader.ex @@ -46,16 +46,20 @@ defmodule MastaniServer.Accounts.Utils.Loader do end def query({"posts_favorites", CMS.PostFavorite}, %{count: _}) do - CMS.PostFavorite |> count_cotents + CMS.PostFavorite |> count_contents end def query({"jobs_favorites", CMS.JobFavorite}, %{count: _}) do - CMS.JobFavorite |> count_cotents + CMS.JobFavorite |> count_contents + end + + def query({"videos_favorites", CMS.VideoFavorite}, %{count: _}) do + CMS.VideoFavorite |> count_contents end def query(queryable, _args), do: queryable - defp count_cotents(queryable) do + defp count_contents(queryable) do queryable |> group_by([f], f.user_id) |> select([f], count(f.id)) diff --git a/lib/mastani_server/cms/job_favorite.ex b/lib/mastani_server/cms/job_favorite.ex index d12581360..d3e9862ac 100644 --- a/lib/mastani_server/cms/job_favorite.ex +++ b/lib/mastani_server/cms/job_favorite.ex @@ -8,19 +8,22 @@ defmodule MastaniServer.CMS.JobFavorite do alias MastaniServer.CMS.Job @required_fields ~w(user_id job_id)a + @optional_fields ~w(category_id)a @type t :: %JobFavorite{} schema "jobs_favorites" do belongs_to(:user, Accounts.User, foreign_key: :user_id) belongs_to(:job, Job, foreign_key: :job_id) + belongs_to(:category, Accounts.FavoriteCategory) + timestamps(type: :utc_datetime) end @doc false def changeset(%JobFavorite{} = job_favorite, attrs) do job_favorite - |> cast(attrs, @required_fields) + |> cast(attrs, @optional_fields ++ @required_fields) |> validate_required(@required_fields) |> unique_constraint(:user_id, name: :jobs_favorites_user_id_job_id_index) end diff --git a/lib/mastani_server/cms/post_favorite.ex b/lib/mastani_server/cms/post_favorite.ex index ae9423442..0ab14843a 100644 --- a/lib/mastani_server/cms/post_favorite.ex +++ b/lib/mastani_server/cms/post_favorite.ex @@ -14,7 +14,7 @@ defmodule MastaniServer.CMS.PostFavorite do schema "posts_favorites" do belongs_to(:user, Accounts.User, foreign_key: :user_id) belongs_to(:post, Post, foreign_key: :post_id) - # has_many(:category, UserFavoriteCategory) + belongs_to(:category, Accounts.FavoriteCategory) timestamps(type: :utc_datetime) diff --git a/lib/mastani_server/cms/video_favorite.ex b/lib/mastani_server/cms/video_favorite.ex index ae885883e..dfbc9a3b7 100644 --- a/lib/mastani_server/cms/video_favorite.ex +++ b/lib/mastani_server/cms/video_favorite.ex @@ -8,19 +8,22 @@ defmodule MastaniServer.CMS.VideoFavorite do alias MastaniServer.CMS.Video @required_fields ~w(user_id video_id)a + @optional_fields ~w(category_id)a @type t :: %VideoFavorite{} schema "videos_favorites" do belongs_to(:user, Accounts.User, foreign_key: :user_id) belongs_to(:video, Video, foreign_key: :video_id) + belongs_to(:category, Accounts.FavoriteCategory) + timestamps(type: :utc_datetime) end @doc false def changeset(%VideoFavorite{} = video_favorite, attrs) do video_favorite - |> cast(attrs, @required_fields) + |> cast(attrs, @optional_fields ++ @required_fields) |> validate_required(@required_fields) |> unique_constraint(:user_id, name: :videos_favorites_user_id_video_id_index) end diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 23c06a5dd..3d5dad3a4 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -94,21 +94,18 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.fetch_followings(cur_user, filter) end - # for check other users query - def favorited_posts(_root, ~m(user_id filter)a, _info) do - Accounts.reacted_contents(:post, :favorite, filter, %User{id: user_id}) - end - def favorited_posts(_root, ~m(filter)a, %{context: %{cur_user: cur_user}}) do - Accounts.reacted_contents(:post, :favorite, filter, cur_user) + # get favorited contents + def favorited_contents(_root, ~m(user_id category_id filter thread)a, _info) do + Accounts.reacted_contents(thread, :favorite, category_id, filter, %User{id: user_id}) end - def favorited_jobs(_root, ~m(user_id filter)a, _info) do - Accounts.reacted_contents(:job, :favorite, filter, %User{id: user_id}) + def favorited_contents(_root, ~m(user_id filter thread)a, _info) do + Accounts.reacted_contents(thread, :favorite, filter, %User{id: user_id}) end - def favorited_jobs(_root, ~m(filter)a, %{context: %{cur_user: cur_user}}) do - Accounts.reacted_contents(:job, :favorite, filter, cur_user) + def favorited_contents(_root, ~m(filter thread)a, %{context: %{cur_user: cur_user}}) do + Accounts.reacted_contents(thread, :favorite, filter, cur_user) end # TODO: refactor diff --git a/lib/mastani_server_web/schema/account/account_queries.ex b/lib/mastani_server_web/schema/account/account_queries.ex index 7e7fd2b65..d6dedd1a0 100644 --- a/lib/mastani_server_web/schema/account/account_queries.ex +++ b/lib/mastani_server_web/schema/account/account_queries.ex @@ -60,7 +60,7 @@ defmodule MastaniServerWeb.Schema.Account.Queries do end @desc "get favorites categoories" - field :list_favorite_categories, :paged_favorites_categories do + field :favorite_categories, :paged_favorites_categories do arg(:user_id, :id) arg(:filter, non_null(:common_paged_filter)) @@ -72,18 +72,33 @@ defmodule MastaniServerWeb.Schema.Account.Queries do field :favorited_posts, :paged_posts do arg(:user_id, :id) arg(:filter, non_null(:paged_filter)) + arg(:category_id, :id) + arg(:thread, :post_thread, default_value: :post) middleware(M.PageSizeProof) - resolve(&R.Accounts.favorited_posts/3) + resolve(&R.Accounts.favorited_contents/3) end @desc "get favorited jobs" field :favorited_jobs, :paged_jobs do arg(:user_id, :id) arg(:filter, non_null(:paged_filter)) + arg(:category_id, :id) + arg(:thread, :job_thread, default_value: :job) middleware(M.PageSizeProof) - resolve(&R.Accounts.favorited_jobs/3) + resolve(&R.Accounts.favorited_contents/3) + end + + @desc "get favorited jobs" + field :favorited_videos, :paged_videos do + arg(:user_id, :id) + arg(:filter, non_null(:paged_filter)) + arg(:category_id, :id) + arg(:thread, :video_thread, default_value: :video) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.favorited_contents/3) end @desc "get all passport rules include system and community etc ..." diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index 155bee8f7..b324e607b 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -110,20 +110,34 @@ defmodule MastaniServerWeb.Schema.Account.Types do middleware(M.ViewerDidConvert) end + @doc "paged favorited posts" field :favorited_posts, :paged_posts do arg(:filter, non_null(:paged_filter)) + arg(:thread, :post_thread, default_value: :post) middleware(M.PageSizeProof) - resolve(&R.Accounts.favorited_posts/3) + resolve(&R.Accounts.favorited_contents/3) end + @doc "paged favorited jobs" field :favorited_jobs, :paged_jobs do arg(:filter, non_null(:paged_filter)) + arg(:thread, :job_thread, default_value: :job) middleware(M.PageSizeProof) - resolve(&R.Accounts.favorited_jobs/3) + resolve(&R.Accounts.favorited_contents/3) end + @doc "paged favorited videos" + field :favorited_videos, :paged_videos do + arg(:filter, non_null(:paged_filter)) + arg(:thread, :video_thread, default_value: :video) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.favorited_contents/3) + end + + @doc "total count of favorited posts count" field :favorited_posts_count, :integer do arg(:count, :count_type, default_value: :count) @@ -131,6 +145,7 @@ defmodule MastaniServerWeb.Schema.Account.Types do middleware(M.ConvertToInt) end + @doc "total count of favorited jobs count" field :favorited_jobs_count, :integer do arg(:count, :count_type, default_value: :count) @@ -138,6 +153,14 @@ defmodule MastaniServerWeb.Schema.Account.Types do middleware(M.ConvertToInt) end + @doc "total count of favorited videos count" + field :favorited_videos_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(Accounts, :favorited_videos)) + middleware(M.ConvertToInt) + end + field :contributes, :contribute_map do resolve(&R.Statistics.list_contributes/3) end @@ -204,6 +227,7 @@ defmodule MastaniServerWeb.Schema.Account.Types do field(:index, :integer) field(:total_count, :integer) field(:private, :boolean) + field(:updated_at, :datetime) end object :paged_favorites_categories do diff --git a/priv/repo/migrations/20181011131417_add_category_id_to_jobs.exs b/priv/repo/migrations/20181011131417_add_category_id_to_jobs.exs new file mode 100644 index 000000000..37b055dc7 --- /dev/null +++ b/priv/repo/migrations/20181011131417_add_category_id_to_jobs.exs @@ -0,0 +1,11 @@ +defmodule MastaniServer.Repo.Migrations.AddCategoryIdToJobs do + use Ecto.Migration + + def change do + alter table(:jobs_favorites) do + add(:category_id, references(:favorite_categories, on_delete: :delete_all)) + end + + create(index(:jobs_favorites, [:category_id])) + end +end diff --git a/priv/repo/migrations/20181011133216_add_category_id_to_videos.exs b/priv/repo/migrations/20181011133216_add_category_id_to_videos.exs new file mode 100644 index 000000000..c7c1ac2fc --- /dev/null +++ b/priv/repo/migrations/20181011133216_add_category_id_to_videos.exs @@ -0,0 +1,11 @@ +defmodule MastaniServer.Repo.Migrations.AddCategoryIdToVideos do + use Ecto.Migration + + def change do + alter table(:videos_favorites) do + add(:category_id, references(:favorite_categories, on_delete: :delete_all)) + end + + create(index(:videos_favorites, [:category_id])) + end +end diff --git a/priv/repo/migrations/20181011133312_add_category_index_to_posts_favoritess.exs b/priv/repo/migrations/20181011133312_add_category_index_to_posts_favoritess.exs new file mode 100644 index 000000000..bf74953d1 --- /dev/null +++ b/priv/repo/migrations/20181011133312_add_category_index_to_posts_favoritess.exs @@ -0,0 +1,7 @@ +defmodule MastaniServer.Repo.Migrations.AddCategoryIndexToPostsFavoritess do + use Ecto.Migration + + def change do + create(index(:posts_favorites, [:category_id])) + end +end diff --git a/test/mastani_server_web/mutation/accounts/favorite_category_test.exs b/test/mastani_server_web/mutation/accounts/favorite_category_test.exs index 1bf060604..7901b5aaa 100644 --- a/test/mastani_server_web/mutation/accounts/favorite_category_test.exs +++ b/test/mastani_server_web/mutation/accounts/favorite_category_test.exs @@ -43,7 +43,12 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do @query """ mutation($id: ID!, $title: String, $desc: String, $private: Boolean) { - updateFavoriteCategory(id: $id, title: $title, desc: $desc, private: $private) { + updateFavoriteCategory( + id: $id + title: $title + desc: $desc + private: $private + ) { id title desc @@ -91,7 +96,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do describe "[Accounts FavoriteCategory set/unset]" do @query """ mutation($id: ID!, $thread: CmsThread, $categoryId: ID!) { - setFavorites(id: $id, thread: $thread, categoryId: $categoryId){ + setFavorites(id: $id, thread: $thread, categoryId: $categoryId) { id title totalCount @@ -114,7 +119,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do @query """ mutation($id: ID!, $thread: CmsThread, $categoryId: ID!) { - unsetFavorites(id: $id, thread: $thread, categoryId: $categoryId){ + unsetFavorites(id: $id, thread: $thread, categoryId: $categoryId) { id title totalCount diff --git a/test/mastani_server_web/query/accounts/favorite_category_test.exs b/test/mastani_server_web/query/accounts/favorite_category_test.exs index 1001a7195..c9524a09d 100644 --- a/test/mastani_server_web/query/accounts/favorite_category_test.exs +++ b/test/mastani_server_web/query/accounts/favorite_category_test.exs @@ -17,7 +17,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavoriteCategory do describe "[Accounts FavoriteCategory]" do @query """ query($userId: ID, $filter: CommonPagedFilter!) { - listFavoriteCategories(userId: $userId, filter: $filter) { + favoriteCategories(userId: $userId, filter: $filter) { entries { id title @@ -35,7 +35,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavoriteCategory do {:ok, _} = Accounts.create_favorite_category(user, %{title: test_category, private: false}) variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "listFavoriteCategories") + results = user_conn |> query_result(@query, variables, "favoriteCategories") assert results |> is_valid_pagination? assert results["totalCount"] == 1 end @@ -47,7 +47,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavoriteCategory do {:ok, _} = Accounts.create_favorite_category(user, %{title: test_category2, private: true}) variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "listFavoriteCategories") + results = user_conn |> query_result(@query, variables, "favoriteCategories") assert results |> is_valid_pagination? assert results["totalCount"] == 2 end @@ -61,7 +61,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavoriteCategory do {:ok, _} = Accounts.create_favorite_category(user, %{title: test_category2, private: true}) variables = %{userId: user.id, filter: %{page: 1, size: 20}} - results = guest_conn |> query_result(@query, variables, "listFavoriteCategories") + results = guest_conn |> query_result(@query, variables, "favoriteCategories") assert results |> is_valid_pagination? assert results["entries"] |> Enum.any?(&(&1["title"] !== test_category2)) diff --git a/test/mastani_server_web/query/accounts/favorited_jobs_test.exs b/test/mastani_server_web/query/accounts/favorited_jobs_test.exs index 96940a8cb..5983d860f 100644 --- a/test/mastani_server_web/query/accounts/favorited_jobs_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_jobs_test.exs @@ -48,8 +48,8 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do end @query """ - query($userId: ID, $filter: PagedFilter!) { - favoritedJobs(userId: $userId, filter: $filter) { + query($userId: ID, $categoryId: ID,$filter: PagedFilter!) { + favoritedJobs(userId: $userId, categoryId: $categoryId, filter: $filter) { entries { id } @@ -83,5 +83,41 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do assert results["totalCount"] == total_count end + + alias MastaniServer.Accounts + test "can get paged favoritedJobs on a spec category", ~m(user_conn guest_conn jobs)a do + {:ok, user} = db_insert(:user) + + Enum.each(jobs, fn job -> + {:ok, _} = CMS.reaction(:job, :favorite, job.id, user) + end) + + job1 = Enum.at(jobs, 0) + job2 = Enum.at(jobs, 1) + job3 = Enum.at(jobs, 2) + job4 = Enum.at(jobs, 4) + + test_category = "test category" + test_category2 = "test category2" + + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, category2} = Accounts.create_favorite_category(user, %{title: test_category2}) + + {:ok, _favorites_category} = Accounts.set_favorites(user, :job, job1.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :job, job2.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :job, job3.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :job, job4.id, category2.id) + + variables = %{userId: user.id, categoryId: category.id, filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "favoritedJobs") + results2 = guest_conn |> query_result(@query, variables, "favoritedJobs") + + assert results["totalCount"] == 3 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(job1.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(job2.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(job3.id))) + + assert results == results2 + end end end diff --git a/test/mastani_server_web/query/accounts/favorited_posts_test.exs b/test/mastani_server_web/query/accounts/favorited_posts_test.exs index 2be33ee67..9c1e2d20e 100644 --- a/test/mastani_server_web/query/accounts/favorited_posts_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_posts_test.exs @@ -48,8 +48,8 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedPosts do end @query """ - query($userId: ID, $filter: PagedFilter!) { - favoritedPosts(userId: $userId, filter: $filter) { + query($userId: ID, $categoryId: ID, $filter: PagedFilter!) { + favoritedPosts(userId: $userId, categoryId: $categoryId, filter: $filter) { entries { id } @@ -83,5 +83,41 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedPosts do assert results["totalCount"] == total_count end + + alias MastaniServer.Accounts + test "can get paged favoritedPosts on a spec category", ~m(user_conn guest_conn posts)a do + {:ok, user} = db_insert(:user) + + Enum.each(posts, fn post -> + {:ok, _} = CMS.reaction(:post, :favorite, post.id, user) + end) + + post1 = Enum.at(posts, 0) + post2 = Enum.at(posts, 1) + post3 = Enum.at(posts, 2) + post4 = Enum.at(posts, 4) + + test_category = "test category" + test_category2 = "test category2" + + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, category2} = Accounts.create_favorite_category(user, %{title: test_category2}) + + {:ok, _favorites_category} = Accounts.set_favorites(user, :post, post1.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :post, post2.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :post, post3.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :post, post4.id, category2.id) + + variables = %{userId: user.id, categoryId: category.id, filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "favoritedPosts") + results2 = guest_conn |> query_result(@query, variables, "favoritedPosts") + + assert results["totalCount"] == 3 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(post1.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(post2.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(post3.id))) + + assert results == results2 + end end end diff --git a/test/mastani_server_web/query/accounts/favorited_videos_test.exs b/test/mastani_server_web/query/accounts/favorited_videos_test.exs new file mode 100644 index 000000000..a4be17f28 --- /dev/null +++ b/test/mastani_server_web/query/accounts/favorited_videos_test.exs @@ -0,0 +1,123 @@ +defmodule MastaniServer.Test.Query.Accounts.FavritedVideos do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, user} = db_insert(:user) + + total_count = 20 + {:ok, videos} = db_insert_multi(:video, total_count) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(guest_conn user_conn user total_count videos)a} + end + + describe "[account favorited videos]" do + @query """ + query($filter: PagedFilter!) { + account { + id + favoritedVideos(filter: $filter) { + entries { + id + } + totalCount + } + favoritedVideosCount + } + } + """ + test "login user can get it's own favoritedVideos", ~m(user_conn user total_count videos)a do + Enum.each(videos, fn video -> + {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) + end) + + random_id = videos |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string + + variables = %{filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "account") + assert results["favoritedVideos"] |> Map.get("totalCount") == total_count + assert results["favoritedVideosCount"] == total_count + + assert results["favoritedVideos"] + |> Map.get("entries") + |> Enum.any?(&(&1["id"] == random_id)) + end + + @query """ + query($userId: ID, $categoryId: ID, $filter: PagedFilter!) { + favoritedVideos(userId: $userId, categoryId: $categoryId, filter: $filter) { + entries { + id + } + totalCount + } + } + """ + test "other user can get other user's paged favoritedVideos", + ~m(user_conn guest_conn total_count videos)a do + {:ok, user} = db_insert(:user) + + Enum.each(videos, fn video -> + {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) + end) + + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "favoritedVideos") + results2 = guest_conn |> query_result(@query, variables, "favoritedVideos") + + assert results["totalCount"] == total_count + assert results2["totalCount"] == total_count + end + + test "login user can get self paged favoritedVideos", ~m(user_conn user videos total_count)a do + Enum.each(videos, fn video -> + {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) + end) + + variables = %{filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "favoritedVideos") + + assert results["totalCount"] == total_count + end + + alias MastaniServer.Accounts + test "can get paged favoritedVideos on a spec category", ~m(user_conn guest_conn videos)a do + {:ok, user} = db_insert(:user) + + Enum.each(videos, fn video -> + {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) + end) + + video1 = Enum.at(videos, 0) + video2 = Enum.at(videos, 1) + video3 = Enum.at(videos, 2) + video4 = Enum.at(videos, 4) + + test_category = "test category" + test_category2 = "test category2" + + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, category2} = Accounts.create_favorite_category(user, %{title: test_category2}) + + {:ok, _favorites_category} = Accounts.set_favorites(user, :video, video1.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :video, video2.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :video, video3.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :video, video4.id, category2.id) + + variables = %{userId: user.id, categoryId: category.id, filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "favoritedVideos") + results2 = guest_conn |> query_result(@query, variables, "favoritedVideos") + + assert results["totalCount"] == 3 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(video1.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(video2.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(video3.id))) + + assert results == results2 + end + end +end From b462963319393b136e887bf09622ca32486464e5 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 11 Oct 2018 22:50:16 +0800 Subject: [PATCH 054/129] style: fmt --- lib/mastani_server/accounts/delegates/favorite_category.ex | 1 + lib/mastani_server_web/resolvers/accounts_resolver.ex | 1 - .../mastani_server_web/query/accounts/favorited_jobs_test.exs | 1 + .../query/accounts/favorited_posts_test.exs | 1 + .../query/accounts/favorited_videos_test.exs | 4 +++- 5 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/mastani_server/accounts/delegates/favorite_category.ex b/lib/mastani_server/accounts/delegates/favorite_category.ex index 132c13659..865a1e52c 100644 --- a/lib/mastani_server/accounts/delegates/favorite_category.ex +++ b/lib/mastani_server/accounts/delegates/favorite_category.ex @@ -84,6 +84,7 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do end alias CMS.{PostFavorite, JobFavorite, VideoFavorite} + @doc """ set category for favorited content (post, job, video ...) """ diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 3d5dad3a4..f754a82ab 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -94,7 +94,6 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.fetch_followings(cur_user, filter) end - # get favorited contents def favorited_contents(_root, ~m(user_id category_id filter thread)a, _info) do Accounts.reacted_contents(thread, :favorite, category_id, filter, %User{id: user_id}) diff --git a/test/mastani_server_web/query/accounts/favorited_jobs_test.exs b/test/mastani_server_web/query/accounts/favorited_jobs_test.exs index 5983d860f..a446f47ab 100644 --- a/test/mastani_server_web/query/accounts/favorited_jobs_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_jobs_test.exs @@ -85,6 +85,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do end alias MastaniServer.Accounts + test "can get paged favoritedJobs on a spec category", ~m(user_conn guest_conn jobs)a do {:ok, user} = db_insert(:user) diff --git a/test/mastani_server_web/query/accounts/favorited_posts_test.exs b/test/mastani_server_web/query/accounts/favorited_posts_test.exs index 9c1e2d20e..658eb3028 100644 --- a/test/mastani_server_web/query/accounts/favorited_posts_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_posts_test.exs @@ -85,6 +85,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedPosts do end alias MastaniServer.Accounts + test "can get paged favoritedPosts on a spec category", ~m(user_conn guest_conn posts)a do {:ok, user} = db_insert(:user) diff --git a/test/mastani_server_web/query/accounts/favorited_videos_test.exs b/test/mastani_server_web/query/accounts/favorited_videos_test.exs index a4be17f28..b3f17bdb4 100644 --- a/test/mastani_server_web/query/accounts/favorited_videos_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_videos_test.exs @@ -73,7 +73,8 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedVideos do assert results2["totalCount"] == total_count end - test "login user can get self paged favoritedVideos", ~m(user_conn user videos total_count)a do + test "login user can get self paged favoritedVideos", + ~m(user_conn user videos total_count)a do Enum.each(videos, fn video -> {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) end) @@ -85,6 +86,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedVideos do end alias MastaniServer.Accounts + test "can get paged favoritedVideos on a spec category", ~m(user_conn guest_conn videos)a do {:ok, user} = db_insert(:user) From 81c749743d3fa0c83139b0896a43a05ece4764c1 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 11 Oct 2018 23:33:10 +0800 Subject: [PATCH 055/129] feat(user achievement): add source contribute info --- lib/mastani_server/accounts/achievement.ex | 4 +++- .../accounts/source_contribute.ex | 24 +++++++++++++++++++ ...513_add_contribute_to_user_achievement.exs | 13 ++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 lib/mastani_server/accounts/source_contribute.ex create mode 100644 priv/repo/migrations/20181011152513_add_contribute_to_user_achievement.exs diff --git a/lib/mastani_server/accounts/achievement.ex b/lib/mastani_server/accounts/achievement.ex index 6faf1b0b7..4ca6d67e3 100644 --- a/lib/mastani_server/accounts/achievement.ex +++ b/lib/mastani_server/accounts/achievement.ex @@ -4,7 +4,7 @@ defmodule MastaniServer.Accounts.Achievement do use Ecto.Schema import Ecto.Changeset - alias MastaniServer.Accounts.User + alias MastaniServer.Accounts.{User, SourceContribute} @required_fields ~w(user_id)a @optional_fields ~w(contents_stared_count contents_favorited_count contents_watched_count followers_count reputation)a @@ -18,6 +18,8 @@ defmodule MastaniServer.Accounts.Achievement do field(:contents_watched_count, :integer, default: 0) field(:followers_count, :integer, default: 0) field(:reputation, :integer, default: 0) + # source_contribute + embeds_one(:source_contribute, SourceContribute, on_replace: :delete) timestamps(type: :utc_datetime) end diff --git a/lib/mastani_server/accounts/source_contribute.ex b/lib/mastani_server/accounts/source_contribute.ex new file mode 100644 index 000000000..da86e0fed --- /dev/null +++ b/lib/mastani_server/accounts/source_contribute.ex @@ -0,0 +1,24 @@ +defmodule MastaniServer.Accounts.SourceContribute do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + + @optional_fields ~w(web server mobile we_app h5)a + + @type t :: %SourceContribute{} + embedded_schema do + field(:web, :boolean) + field(:server, :boolean) + field(:mobile, :boolean) + field(:we_app, :boolean) + field(:h5, :boolean) + end + + @doc false + def changeset(%SourceContribute{} = source_contribute, attrs) do + source_contribute + |> cast(attrs, @optional_fields) + end +end diff --git a/priv/repo/migrations/20181011152513_add_contribute_to_user_achievement.exs b/priv/repo/migrations/20181011152513_add_contribute_to_user_achievement.exs new file mode 100644 index 000000000..0ad010d3f --- /dev/null +++ b/priv/repo/migrations/20181011152513_add_contribute_to_user_achievement.exs @@ -0,0 +1,13 @@ +defmodule MastaniServer.Repo.Migrations.AddContributeToUserAchievement do + use Ecto.Migration + + def change do + alter table(:user_achievements) do + add( + :source_contribute, + :map, + default: %{web: false, server: false, mobile: false, we_app: false, h5: false} + ) + end + end +end From d93a11bbb1b45424b17c4f33162b764ea97783f5 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 12 Oct 2018 09:46:06 +0800 Subject: [PATCH 056/129] feat(user signin): init a empty achievement when new signin --- .../accounts/delegates/profile.ex | 7 ++++--- .../mastani_server/accounts/accounts_test.exs | 21 +++++++++++++++++++ .../query/accounts/achievement_test.exs | 7 ------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/mastani_server/accounts/delegates/profile.ex b/lib/mastani_server/accounts/delegates/profile.ex index b739b2413..6783ef179 100644 --- a/lib/mastani_server/accounts/delegates/profile.ex +++ b/lib/mastani_server/accounts/delegates/profile.ex @@ -7,7 +7,7 @@ defmodule MastaniServer.Accounts.Delegate.Profile do import ShortMaps alias Helper.{RadarSearch, Guardian, ORM, QueryBuilder} - alias MastaniServer.Accounts.{GithubUser, User} + alias MastaniServer.Accounts.{Achievement, GithubUser, User} alias MastaniServer.{CMS, Repo} alias Ecto.Multi @@ -56,11 +56,9 @@ defmodule MastaniServer.Accounts.Delegate.Profile do case ORM.find_by(GithubUser, github_id: to_string(github_user["id"])) do {:ok, g_user} -> {:ok, user} = ORM.find(User, g_user.user_id) - # IO.inspect label: "send back from db" token_info(user) {:error, _} -> - # IO.inspect label: "register then send" register_github_user(github_user, remote_ip) end end @@ -94,6 +92,9 @@ defmodule MastaniServer.Accounts.Delegate.Profile do |> Multi.run(:create_profile, fn %{create_user: user} -> create_profile(user, github_profile, :github) end) + |> Multi.run(:init_achievement, fn %{create_user: user} -> + Achievement |> ORM.upsert_by([user_id: user.id], %{user_id: user.id}) + end) |> Repo.transaction() |> register_github_result(remote_ip) end diff --git a/test/mastani_server/accounts/accounts_test.exs b/test/mastani_server/accounts/accounts_test.exs index d379e3a9e..903d0761b 100644 --- a/test/mastani_server/accounts/accounts_test.exs +++ b/test/mastani_server/accounts/accounts_test.exs @@ -120,6 +120,27 @@ defmodule MastaniServer.Test.Accounts do assert g_user.node_id == @valid_github_profile["node_id"] end + test "new user have a empty achievement" do + assert {:error, _} = + ORM.find_by(GithubUser, github_id: to_string(@valid_github_profile["id"])) + + assert {:error, _} = ORM.find_by(User, nickname: @valid_github_profile["login"]) + + {:ok, %{token: token, user: _user}} = Accounts.github_signin(@valid_github_profile) + {:ok, claims, _info} = Guardian.jwt_decode(token) + + {:ok, created_user} = ORM.find(User, claims.id, preload: :achievement) + achievement = created_user.achievement + assert achievement.user_id == created_user.id + assert achievement.reputation == 0 + assert achievement.contents_favorited_count == 0 + assert achievement.contents_stared_count == 0 + assert achievement.source_contribute.h5 == false + assert achievement.source_contribute.web == false + assert achievement.source_contribute.we_app == false + assert achievement.source_contribute.mobile == false + end + test "exsit github user created twice fails" do assert ORM.count(GithubUser) == 0 {:ok, _} = Accounts.github_signin(@valid_github_profile) diff --git a/test/mastani_server_web/query/accounts/achievement_test.exs b/test/mastani_server_web/query/accounts/achievement_test.exs index f5e87c6d6..0e21d201f 100644 --- a/test/mastani_server_web/query/accounts/achievement_test.exs +++ b/test/mastani_server_web/query/accounts/achievement_test.exs @@ -30,13 +30,6 @@ defmodule MastaniServer.Test.Query.Account.Achievement do } } """ - test "new user has no acheiveements", ~m(guest_conn user)a do - variables = %{id: user.id} - results = guest_conn |> query_result(@query, variables, "user") - - assert is_nil(results["achievement"]) - end - test "inc user's achievement after user got followed", ~m(guest_conn user)a do {:ok, user2} = db_insert(:user) {:ok, user3} = db_insert(:user) From c45b72bf07e4ddc4a742d02d14e1540b3380ecd8 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 12 Oct 2018 10:09:02 +0800 Subject: [PATCH 057/129] feat(favorite cats): add last_updated field record time when create/update set/unset catetory --- .../accounts/delegates/favorite_category.ex | 30 ++++++++++++++----- .../accounts/favorite_category.ex | 6 ++-- .../schema/account/account_types.ex | 2 ++ ...dd_last_updated_to_favorite_categories.exs | 9 ++++++ .../accounts/favorite_category_test.exs | 9 ++++++ 5 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 priv/repo/migrations/20181012014834_add_last_updated_to_favorite_categories.exs diff --git a/lib/mastani_server/accounts/delegates/favorite_category.ex b/lib/mastani_server/accounts/delegates/favorite_category.ex index 865a1e52c..3fb7923e4 100644 --- a/lib/mastani_server/accounts/delegates/favorite_category.ex +++ b/lib/mastani_server/accounts/delegates/favorite_category.ex @@ -17,7 +17,8 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do def create_favorite_category(%User{id: user_id}, %{title: title} = attrs) do with {:error, _} <- FavoriteCategory |> ORM.find_by(~m(user_id title)a) do - FavoriteCategory |> ORM.create(attrs |> Map.merge(~m(user_id)a)) + last_updated = Timex.today() |> Timex.to_datetime() + FavoriteCategory |> ORM.create(Map.merge(~m(user_id last_updated)a, attrs)) else {:ok, category} -> {:error, [message: "#{category.title} already exsits", code: ecode(:already_exsit)]} @@ -26,7 +27,8 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do def update_favorite_category(%User{id: user_id}, %{id: id} = attrs) do with {:ok, category} <- FavoriteCategory |> ORM.find_by(~m(id user_id)a) do - category |> ORM.update(attrs) + last_updated = Timex.today() |> Timex.to_datetime() + category |> ORM.update(Map.merge(~m(last_updated)a, attrs)) end end @@ -103,15 +105,21 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do content_favorite |> ORM.update(%{category_id: favorite_category.id}) end) - |> Multi.run(:inc_count, fn _ -> - favorite_category |> ORM.update(%{total_count: favorite_category.total_count + 1}) + |> Multi.run(:update_category_info, fn _ -> + last_updated = Timex.today() |> Timex.to_datetime() + + favorite_category + |> ORM.update(%{ + last_updated: last_updated, + total_count: favorite_category.total_count + 1 + }) end) |> Repo.transaction() |> set_favorites_result() end end - defp set_favorites_result({:ok, %{inc_count: result}}), do: {:ok, result} + defp set_favorites_result({:ok, %{update_category_info: result}}), do: {:ok, result} defp set_favorites_result({:error, :favorite_content, result, _steps}) do # {:error, [message: "favorite content fails", code: ecode(:react_fails)]} @@ -135,8 +143,14 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do content_favorite |> ORM.delete() end) - |> Multi.run(:dec_count, fn _ -> - favorite_category |> ORM.update(%{total_count: max(favorite_category.total_count - 1, 0)}) + |> Multi.run(:update_category_info, fn _ -> + last_updated = Timex.today() |> Timex.to_datetime() + + favorite_category + |> ORM.update(%{ + last_updated: last_updated, + total_count: max(favorite_category.total_count - 1, 0) + }) end) |> Repo.transaction() |> unset_favorites_result() @@ -144,7 +158,7 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do end # @spec unset_favorites_result({:ok, map()}) :: {:ok, FavoriteCategory.t() } - defp unset_favorites_result({:ok, %{dec_count: result}}), do: {:ok, result} + defp unset_favorites_result({:ok, %{update_category_info: result}}), do: {:ok, result} defp unset_favorites_result({:error, :remove_favorite_record, result, _steps}) do # {:error, [message: "favorite content fails", code: ecode(:react_fails)]} diff --git a/lib/mastani_server/accounts/favorite_category.ex b/lib/mastani_server/accounts/favorite_category.ex index 96133c891..15f829173 100644 --- a/lib/mastani_server/accounts/favorite_category.ex +++ b/lib/mastani_server/accounts/favorite_category.ex @@ -7,7 +7,7 @@ defmodule MastaniServer.Accounts.FavoriteCategory do alias MastaniServer.Accounts.User @required_fields ~w(user_id title)a - @optional_fields ~w(index total_count private desc)a + @optional_fields ~w(index total_count private desc last_updated)a @type t :: %FavoriteCategory{} schema "favorite_categories" do @@ -19,8 +19,8 @@ defmodule MastaniServer.Accounts.FavoriteCategory do field(:index, :integer) field(:total_count, :integer, default: 0) field(:private, :boolean, default: false) - # TODO: - # last_updated + # last time when add/delete items in category + field(:last_updated, :utc_datetime) timestamps(type: :utc_datetime) end diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index b324e607b..91059417a 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -227,6 +227,8 @@ defmodule MastaniServerWeb.Schema.Account.Types do field(:index, :integer) field(:total_count, :integer) field(:private, :boolean) + field(:last_updated, :datetime) + field(:inserted_at, :datetime) field(:updated_at, :datetime) end diff --git a/priv/repo/migrations/20181012014834_add_last_updated_to_favorite_categories.exs b/priv/repo/migrations/20181012014834_add_last_updated_to_favorite_categories.exs new file mode 100644 index 000000000..ef75bfbf7 --- /dev/null +++ b/priv/repo/migrations/20181012014834_add_last_updated_to_favorite_categories.exs @@ -0,0 +1,9 @@ +defmodule MastaniServer.Repo.Migrations.AddLastUpdatedToFavoriteCategories do + use Ecto.Migration + + def change do + alter table(:favorite_categories) do + add(:last_updated, :utc_datetime) + end + end +end diff --git a/test/mastani_server_web/mutation/accounts/favorite_category_test.exs b/test/mastani_server_web/mutation/accounts/favorite_category_test.exs index 7901b5aaa..ebbe3da41 100644 --- a/test/mastani_server_web/mutation/accounts/favorite_category_test.exs +++ b/test/mastani_server_web/mutation/accounts/favorite_category_test.exs @@ -22,6 +22,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do createFavoriteCategory(title: $title, private: $private) { id title + lastUpdated } } """ @@ -32,6 +33,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do {:ok, found} = FavoriteCategory |> ORM.find(created |> Map.get("id")) assert created |> Map.get("id") == to_string(found.id) + assert created["lastUpdated"] != nil end test "unauth user create category fails", ~m(guest_conn)a do @@ -53,6 +55,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do title desc private + lastUpdated } } """ @@ -72,6 +75,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do assert updated["desc"] == "new desc" assert updated["private"] == true assert updated["title"] == "new title" + assert updated["lastUpdated"] != nil end @query """ @@ -100,6 +104,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do id title totalCount + lastUpdated } } """ @@ -112,6 +117,8 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do {:ok, found} = CMS.PostFavorite |> ORM.find_by(%{post_id: post.id, user_id: user.id}) assert created["totalCount"] == 1 + assert created["lastUpdated"] != nil + assert found.category_id == category.id assert found.user_id == user.id assert found.post_id == post.id @@ -123,6 +130,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do id title totalCount + lastUpdated } } """ @@ -133,6 +141,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do {:ok, category} = Accounts.FavoriteCategory |> ORM.find(category.id) assert category.total_count == 1 + assert category.last_updated != nil variables = %{id: post.id, categoryId: category.id} user_conn |> mutation_result(@query, variables, "unsetFavorites") From 1cee80b7c826a5c6889f8352c26415c4ffd25071 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 12 Oct 2018 12:09:14 +0800 Subject: [PATCH 058/129] feat(user): add editable-communities query field --- lib/mastani_server/accounts/accounts.ex | 2 + .../accounts/delegates/achievements.ex | 19 ++++- .../resolvers/accounts_resolver.ex | 9 ++ .../schema/account/account_queries.ex | 9 ++ .../schema/account/account_types.ex | 8 ++ .../accounts/achievement_test.exs | 31 +++++++ .../query/accounts/achievement_test.exs | 82 ++++++++++++++++++- test/support/assert_helper.ex | 11 +++ 8 files changed, 169 insertions(+), 2 deletions(-) diff --git a/lib/mastani_server/accounts/accounts.ex b/lib/mastani_server/accounts/accounts.ex index 8696c4d2e..41a74696f 100644 --- a/lib/mastani_server/accounts/accounts.ex +++ b/lib/mastani_server/accounts/accounts.ex @@ -29,6 +29,8 @@ defmodule MastaniServer.Accounts do # achievement defdelegate achieve(user, operation, key), to: Achievements + defdelegate list_editable_communities(user, filter), to: Achievements + # defdelegate list_editable_communities(filter), to: Achievements # fans defdelegate follow(user, follower), to: Fans diff --git a/lib/mastani_server/accounts/delegates/achievements.ex b/lib/mastani_server/accounts/delegates/achievements.ex index 4f2c483c2..d37fa0318 100644 --- a/lib/mastani_server/accounts/delegates/achievements.ex +++ b/lib/mastani_server/accounts/delegates/achievements.ex @@ -7,7 +7,8 @@ defmodule MastaniServer.Accounts.Delegate.Achievements do 3. create content been favorited by other user + 2 4. followed by other user + 3 """ - import Helper.Utils, only: [get_config: 2] + import Ecto.Query, warn: false + import Helper.Utils, only: [get_config: 2, done: 1] import ShortMaps alias Helper.{ORM, SpecType} @@ -121,4 +122,20 @@ defmodule MastaniServer.Accounts.Delegate.Achievements do count - unit end end + + @doc """ + list communities which the user is editor in it + """ + alias MastaniServer.CMS.CommunityEditor + + def list_editable_communities(%User{id: user_id}, %{page: page, size: size}) do + with {:ok, user} <- ORM.find(User, user_id) do + CommunityEditor + |> where([e], e.user_id == ^user.id) + |> join(:inner, [e], c in assoc(e, :community)) + |> select([e, c], c) + |> ORM.paginater(page: page, size: size) + |> done() + end + end end diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index f754a82ab..4962bcc68 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -107,6 +107,15 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.reacted_contents(thread, :favorite, filter, cur_user) end + # paged communities which the user it's the editor + def editable_communities(_root, ~m(user_id filter)a, _info) do + Accounts.list_editable_communities(%User{id: user_id}, filter) + end + + def editable_communities(_root, ~m(filter)a, %{context: %{cur_user: cur_user}}) do + Accounts.list_editable_communities(cur_user, filter) + end + # TODO: refactor def get_mail_box_status(_root, _args, %{context: %{cur_user: cur_user}}) do Accounts.mailbox_status(cur_user) diff --git a/lib/mastani_server_web/schema/account/account_queries.ex b/lib/mastani_server_web/schema/account/account_queries.ex index d6dedd1a0..17da604a7 100644 --- a/lib/mastani_server_web/schema/account/account_queries.ex +++ b/lib/mastani_server_web/schema/account/account_queries.ex @@ -101,6 +101,15 @@ defmodule MastaniServerWeb.Schema.Account.Queries do resolve(&R.Accounts.favorited_contents/3) end + @desc "paged communities which the user it's the editor" + field :editable_communities, :paged_communities do + arg(:user_id, :id) + arg(:filter, non_null(:paged_filter)) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.editable_communities/3) + end + @desc "get all passport rules include system and community etc ..." field :all_passport_rules_string, :rules do middleware(M.Authorize, :login) diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index 91059417a..bc999b61a 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -77,6 +77,14 @@ defmodule MastaniServerWeb.Schema.Account.Types do middleware(M.ConvertToInt) end + @desc "paged communities which the user it's the editor" + field :editable_communities, :paged_communities do + arg(:filter, non_null(:paged_filter)) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.editable_communities/3) + end + # NOTE: dataloader not work at this case # field :followers_count, :integer do # arg(:count, :count_type, default_value: :count) diff --git a/test/mastani_server/accounts/achievement_test.exs b/test/mastani_server/accounts/achievement_test.exs index 2ac990946..214b54783 100644 --- a/test/mastani_server/accounts/achievement_test.exs +++ b/test/mastani_server/accounts/achievement_test.exs @@ -17,6 +17,37 @@ defmodule MastaniServer.Test.Accounts.Achievement do {:ok, ~m(user)a} end + alias MastaniServer.CMS + + describe "[Accounts Achievement communities]" do + test "normal user should have a empty editable communities list", ~m(user)a do + {:ok, results} = Accounts.list_editable_communities(user, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == 0 + end + + test "community editor should get a editable community list", ~m(user)a do + title = "chief editor" + {:ok, community} = db_insert(:community) + {:ok, community2} = db_insert(:community) + + {:ok, _} = CMS.set_editor(community, title, user) + {:ok, _} = CMS.set_editor(community2, title, user) + + # bad boy + {:ok, community_x} = db_insert(:community) + {:ok, user_x} = db_insert(:user) + {:ok, _} = CMS.set_editor(community_x, title, user_x) + + {:ok, editable_communities} = Accounts.list_editable_communities(user, %{page: 1, size: 20}) + + assert editable_communities.total_count == 2 + assert editable_communities.entries |> Enum.any?(&(&1.id == community.id)) + assert editable_communities.entries |> Enum.any?(&(&1.id == community2.id)) + end + end + describe "[Accounts Achievement funtion]" do alias Accounts.Achievement diff --git a/test/mastani_server_web/query/accounts/achievement_test.exs b/test/mastani_server_web/query/accounts/achievement_test.exs index 0e21d201f..e800e7fc9 100644 --- a/test/mastani_server_web/query/accounts/achievement_test.exs +++ b/test/mastani_server_web/query/accounts/achievement_test.exs @@ -1,7 +1,7 @@ defmodule MastaniServer.Test.Query.Account.Achievement do use MastaniServer.TestTools import Helper.Utils, only: [get_config: 2] - alias MastaniServer.Accounts + alias MastaniServer.{Accounts, CMS} alias Helper.ORM @@ -17,6 +17,85 @@ defmodule MastaniServer.Test.Query.Account.Achievement do {:ok, ~m(user_conn guest_conn user)a} end + describe "[account editable-communities]" do + @query """ + query($userId: ID, $filter: PagedFilter!) { + editableCommunities(userId: $userId, filter: $filter) { + entries { + id + logo + title + raw + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + @tag :wip + test "can get user's empty editable communities list", ~m(guest_conn user)a do + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "editableCommunities") + + assert results |> is_valid_pagination?(:empty) + end + + @tag :wip + test "can get user's editable communities list when user is editor", ~m(guest_conn user)a do + {:ok, community} = db_insert(:community) + {:ok, community2} = db_insert(:community) + + title = "chief editor" + {:ok, _} = CMS.set_editor(community, title, user) + {:ok, _} = CMS.set_editor(community2, title, user) + + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "editableCommunities") + + assert results["totalCount"] == 2 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(community.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(community2.id))) + end + + @query """ + query($filter: PagedFilter!) { + account { + id + editableCommunities(filter: $filter) { + entries { + id + logo + title + raw + } + totalCount + } + } + } + """ + @tag :wip + test "user can get own editable communities list", ~m(user)a do + user_conn = simu_conn(:user, user) + + {:ok, community} = db_insert(:community) + {:ok, community2} = db_insert(:community) + + title = "chief editor" + {:ok, _} = CMS.set_editor(community, title, user) + {:ok, _} = CMS.set_editor(community2, title, user) + + variables = %{filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "account") + editable_communities = results["editableCommunities"] + + assert editable_communities["totalCount"] == 2 + assert editable_communities["entries"] |> Enum.any?(&(&1["id"] == to_string(community.id))) + assert editable_communities["entries"] |> Enum.any?(&(&1["id"] == to_string(community2.id))) + end + end + describe "[account follow achieveMent]" do @query """ query($id: ID!) { @@ -30,6 +109,7 @@ defmodule MastaniServer.Test.Query.Account.Achievement do } } """ + @tag :wip2 test "inc user's achievement after user got followed", ~m(guest_conn user)a do {:ok, user2} = db_insert(:user) {:ok, user3} = db_insert(:user) diff --git a/test/support/assert_helper.ex b/test/support/assert_helper.ex index d2aec7994..7e3c69570 100644 --- a/test/support/assert_helper.ex +++ b/test/support/assert_helper.ex @@ -52,6 +52,17 @@ defmodule MastaniServer.Test.AssertHelper do is_valid_kv?(obj, "pageNumber", :int) end + def is_valid_pagination?(obj, :empty) when is_map(obj) do + case is_valid_pagination?(obj) do + false -> + false + + true -> + obj["entries"] |> Enum.empty?() and obj["totalCount"] == 0 and obj["pageNumber"] == 1 and + obj["totalPages"] == 1 + end + end + def is_valid_pagination?(obj, :raw) when is_map(obj) do is_valid_kv?(obj, "entries", :list) and is_valid_kv?(obj, "total_pages", :int) and is_valid_kv?(obj, "total_count", :int) and is_valid_kv?(obj, "page_size", :int) and From f0addbfec3cd66197728f53437390937488823d2 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 12 Oct 2018 18:00:51 +0800 Subject: [PATCH 059/129] refactor(user query): rm unneed filter on some query --- .../middleware/pagesize_proof.ex | 3 ++- .../resolvers/accounts_resolver.ex | 4 ++++ .../schema/account/account_types.ex | 14 +++++++++++++- .../query/accounts/achievement_test.exs | 7 +++---- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/mastani_server_web/middleware/pagesize_proof.ex b/lib/mastani_server_web/middleware/pagesize_proof.ex index 77533cd87..2d884d7d7 100644 --- a/lib/mastani_server_web/middleware/pagesize_proof.ex +++ b/lib/mastani_server_web/middleware/pagesize_proof.ex @@ -41,7 +41,8 @@ defmodule MastaniServerWeb.Middleware.PageSizeProof do defp valid_size(%{filter: %{first: size}} = arg), do: do_size_check(size, arg) defp valid_size(%{filter: %{size: size}} = arg), do: do_size_check(size, arg) - defp valid_size(arg), do: arg |> Map.merge(%{filter: %{first: @inner_page_size}}) + # defp valid_size(arg), do: arg |> Map.merge(%{filter: %{first: @inner_page_size}}) + defp valid_size(arg), do: arg |> Map.merge(%{filter: %{page: 1, size: @max_page_size, first: @inner_page_size}}) defp do_size_check(size, arg) do case size in 1..@max_page_size do diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 4962bcc68..48909ed16 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -112,6 +112,10 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.list_editable_communities(%User{id: user_id}, filter) end + def editable_communities(root, ~m(filter)a, _info) do + Accounts.list_editable_communities(%User{id: root.id}, filter) + end + def editable_communities(_root, ~m(filter)a, %{context: %{cur_user: cur_user}}) do Accounts.list_editable_communities(cur_user, filter) end diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index bc999b61a..6679100a8 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -79,8 +79,10 @@ defmodule MastaniServerWeb.Schema.Account.Types do @desc "paged communities which the user it's the editor" field :editable_communities, :paged_communities do - arg(:filter, non_null(:paged_filter)) + # arg(:filter, non_null(:paged_filter)) + arg(:filter, :paged_filter) + # middleware(M.SeeMe) middleware(M.PageSizeProof) resolve(&R.Accounts.editable_communities/3) end @@ -245,12 +247,22 @@ defmodule MastaniServerWeb.Schema.Account.Types do pagination_fields() end + object :source_contribute do + field(:web, :boolean) + field(:server, :boolean) + field(:we_app, :boolean) + field(:h5, :boolean) + field(:mobile, :boolean) + end + object :achievement do field(:reputation, :integer) # field(:followers_count, :integer) field(:contents_stared_count, :integer) field(:contents_favorited_count, :integer) # field(:contents_watched_count, :integer) + + field(:source_contribute, :source_contribute) end object :token_info do diff --git a/test/mastani_server_web/query/accounts/achievement_test.exs b/test/mastani_server_web/query/accounts/achievement_test.exs index e800e7fc9..3ee54cef0 100644 --- a/test/mastani_server_web/query/accounts/achievement_test.exs +++ b/test/mastani_server_web/query/accounts/achievement_test.exs @@ -60,10 +60,10 @@ defmodule MastaniServer.Test.Query.Account.Achievement do end @query """ - query($filter: PagedFilter!) { + query { account { id - editableCommunities(filter: $filter) { + editableCommunities { entries { id logo @@ -75,7 +75,7 @@ defmodule MastaniServer.Test.Query.Account.Achievement do } } """ - @tag :wip + @tag :wip2 test "user can get own editable communities list", ~m(user)a do user_conn = simu_conn(:user, user) @@ -109,7 +109,6 @@ defmodule MastaniServer.Test.Query.Account.Achievement do } } """ - @tag :wip2 test "inc user's achievement after user got followed", ~m(guest_conn user)a do {:ok, user2} = db_insert(:user) {:ok, user3} = db_insert(:user) From 1906d2b27d9931aa6056f9e5631311f572541ab8 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 12 Oct 2018 20:08:11 +0800 Subject: [PATCH 060/129] feat(user page): user published contents done right --- lib/mastani_server/accounts/accounts.ex | 4 + .../accounts/delegates/publish.ex | 33 +++ .../middleware/pagesize_proof.ex | 3 +- .../resolvers/accounts_resolver.ex | 15 +- .../schema/account/account_queries.ex | 40 ++++ .../accounts/published_contents_test.exs | 208 ++++++++++++++++++ .../accounts/published_contents_test.exs | 177 +++++++++++++++ 7 files changed, 476 insertions(+), 4 deletions(-) create mode 100644 lib/mastani_server/accounts/delegates/publish.ex create mode 100644 test/mastani_server/accounts/published_contents_test.exs create mode 100644 test/mastani_server_web/query/accounts/published_contents_test.exs diff --git a/lib/mastani_server/accounts/accounts.ex b/lib/mastani_server/accounts/accounts.ex index 41a74696f..ce0488349 100644 --- a/lib/mastani_server/accounts/accounts.ex +++ b/lib/mastani_server/accounts/accounts.ex @@ -7,6 +7,7 @@ defmodule MastaniServer.Accounts do Customization, Fans, FavoriteCategory, + Publish, Mails, Profile, ReactedContents @@ -32,6 +33,9 @@ defmodule MastaniServer.Accounts do defdelegate list_editable_communities(user, filter), to: Achievements # defdelegate list_editable_communities(filter), to: Achievements + # publish + defdelegate published_contents(user, thread, filter), to: Publish + # fans defdelegate follow(user, follower), to: Fans defdelegate undo_follow(user, follower), to: Fans diff --git a/lib/mastani_server/accounts/delegates/publish.ex b/lib/mastani_server/accounts/delegates/publish.ex new file mode 100644 index 000000000..4e9054c29 --- /dev/null +++ b/lib/mastani_server/accounts/delegates/publish.ex @@ -0,0 +1,33 @@ +defmodule MastaniServer.Accounts.Delegate.Publish do + @moduledoc """ + user followers / following related + """ + import Ecto.Query, warn: false + import Helper.Utils, only: [done: 1] + import Helper.ErrorCode + import ShortMaps + + import MastaniServer.CMS.Utils.Matcher + + alias Helper.{ORM, QueryBuilder, SpecType} + alias MastaniServer.{Accounts, Repo} + + alias MastaniServer.Accounts.{User} + alias MastaniServer.CMS + + @doc """ + get paged published contets of a user + """ + def published_contents(%User{id: user_id}, thread, %{page: page, size: size} = filter) do + with {:ok, user} <- ORM.find(User, user_id), + {:ok, content} <- match_action(thread, :self) do + content.target + |> join(:inner, [p], a in assoc(p, :author)) + |> where([p, a], a.user_id == ^user.id) + |> select([p, a], p) + |> QueryBuilder.filter_pack(filter) + |> ORM.paginater(~m(page size)a) + |> done() + end + end +end diff --git a/lib/mastani_server_web/middleware/pagesize_proof.ex b/lib/mastani_server_web/middleware/pagesize_proof.ex index 2d884d7d7..9a6901a60 100644 --- a/lib/mastani_server_web/middleware/pagesize_proof.ex +++ b/lib/mastani_server_web/middleware/pagesize_proof.ex @@ -42,7 +42,8 @@ defmodule MastaniServerWeb.Middleware.PageSizeProof do defp valid_size(%{filter: %{size: size}} = arg), do: do_size_check(size, arg) # defp valid_size(arg), do: arg |> Map.merge(%{filter: %{first: @inner_page_size}}) - defp valid_size(arg), do: arg |> Map.merge(%{filter: %{page: 1, size: @max_page_size, first: @inner_page_size}}) + defp valid_size(arg), + do: arg |> Map.merge(%{filter: %{page: 1, size: @max_page_size, first: @inner_page_size}}) defp do_size_check(size, arg) do case size in 1..@max_page_size do diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 48909ed16..87b3067f8 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -107,14 +107,23 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.reacted_contents(thread, :favorite, filter, cur_user) end + # published contents + def published_contents(_root, ~m(user_id filter thread)a, _info) do + Accounts.published_contents(%User{id: user_id}, thread, filter) + end + + def published_contents(_root, ~m(filter thread)a, %{context: %{cur_user: cur_user}}) do + Accounts.published_contents(cur_user, thread, filter) + end + # paged communities which the user it's the editor def editable_communities(_root, ~m(user_id filter)a, _info) do Accounts.list_editable_communities(%User{id: user_id}, filter) end - def editable_communities(root, ~m(filter)a, _info) do - Accounts.list_editable_communities(%User{id: root.id}, filter) - end + # def editable_communities(root, ~m(filter)a, _info) do + # Accounts.list_editable_communities(%User{id: root.id}, filter) + # end def editable_communities(_root, ~m(filter)a, %{context: %{cur_user: cur_user}}) do Accounts.list_editable_communities(cur_user, filter) diff --git a/lib/mastani_server_web/schema/account/account_queries.ex b/lib/mastani_server_web/schema/account/account_queries.ex index 17da604a7..c43f904e0 100644 --- a/lib/mastani_server_web/schema/account/account_queries.ex +++ b/lib/mastani_server_web/schema/account/account_queries.ex @@ -101,6 +101,46 @@ defmodule MastaniServerWeb.Schema.Account.Queries do resolve(&R.Accounts.favorited_contents/3) end + @desc "get paged published posts" + field :published_posts, :paged_posts do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:thread, :post_thread, default_value: :post) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.published_contents/3) + end + + @desc "get paged published jobs" + field :published_jobs, :paged_jobs do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:thread, :job_thread, default_value: :job) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.published_contents/3) + end + + @desc "get paged published videos" + field :published_videos, :paged_videos do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:thread, :video_thread, default_value: :video) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.published_contents/3) + end + + @desc "get paged published repos" + field :published_repos, :paged_repos do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:thread, :repo_thread, default_value: :repo) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.published_contents/3) + end + @desc "paged communities which the user it's the editor" field :editable_communities, :paged_communities do arg(:user_id, :id) diff --git a/test/mastani_server/accounts/published_contents_test.exs b/test/mastani_server/accounts/published_contents_test.exs new file mode 100644 index 000000000..d9ffbb3af --- /dev/null +++ b/test/mastani_server/accounts/published_contents_test.exs @@ -0,0 +1,208 @@ +defmodule MastaniServer.Test.Accounts.PublishedContents do + use MastaniServer.TestTools + + import Helper.Utils, only: [get_config: 2] + alias Helper.ORM + + alias MastaniServer.{Accounts, CMS} + alias MastaniServer.Accounts.User + + @publish_count 10 + + setup do + {:ok, user} = db_insert(:user) + {:ok, user2} = db_insert(:user) + {:ok, community} = db_insert(:community) + {:ok, community2} = db_insert(:community) + + {:ok, ~m(user user2 community community2)a} + end + + describe "[Accounts Publised posts]" do + @tag :wip + test "fresh user get empty paged published posts", ~m(user)a do + {:ok, results} = Accounts.published_contents(user, :post, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == 0 + end + + @tag :wip + test "user can get paged published posts", ~m(user user2 community community2)a do + pub_posts = + Enum.reduce(1..@publish_count, [], fn _, acc -> + post_attrs = mock_attrs(:post, %{community_id: community.id}) + {:ok, post} = CMS.create_content(community, :post, post_attrs, user) + + acc ++ [post] + end) + + pub_posts2 = + Enum.reduce(1..@publish_count, [], fn _, acc -> + post_attrs = mock_attrs(:post, %{community_id: community2.id}) + {:ok, post} = CMS.create_content(community, :post, post_attrs, user) + + acc ++ [post] + end) + + # unrelated other user + Enum.reduce(1..5, [], fn _, acc -> + post_attrs = mock_attrs(:post, %{community_id: community.id}) + {:ok, post} = CMS.create_content(community, :post, post_attrs, user2) + + acc ++ [post] + end) + + {:ok, results} = Accounts.published_contents(user, :post, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == @publish_count * 2 + + random_post_id = pub_posts |> Enum.random() |> Map.get(:id) + random_post_id2 = pub_posts2 |> Enum.random() |> Map.get(:id) + assert results.entries |> Enum.any?(&(&1.id == random_post_id)) + assert results.entries |> Enum.any?(&(&1.id == random_post_id2)) + end + end + + describe "[Accounts Publised jobs]" do + @tag :wip + test "fresh user get empty paged published jobs", ~m(user)a do + {:ok, results} = Accounts.published_contents(user, :job, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == 0 + end + + @tag :wip + test "user can get paged published jobs", ~m(user user2 community community2)a do + pub_jobs = + Enum.reduce(1..@publish_count, [], fn _, acc -> + job_attrs = mock_attrs(:job, %{community_id: community.id}) + {:ok, job} = CMS.create_content(community, :job, job_attrs, user) + + acc ++ [job] + end) + + pub_jobs2 = + Enum.reduce(1..@publish_count, [], fn _, acc -> + job_attrs = mock_attrs(:job, %{community_id: community2.id}) + {:ok, job} = CMS.create_content(community, :job, job_attrs, user) + + acc ++ [job] + end) + + # unrelated other user + Enum.reduce(1..5, [], fn _, acc -> + job_attrs = mock_attrs(:job, %{community_id: community.id}) + {:ok, job} = CMS.create_content(community, :job, job_attrs, user2) + + acc ++ [job] + end) + + {:ok, results} = Accounts.published_contents(user, :job, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == @publish_count * 2 + + random_job_id = pub_jobs |> Enum.random() |> Map.get(:id) + random_job_id2 = pub_jobs2 |> Enum.random() |> Map.get(:id) + assert results.entries |> Enum.any?(&(&1.id == random_job_id)) + assert results.entries |> Enum.any?(&(&1.id == random_job_id2)) + end + end + + describe "[Accounts Publised videos]" do + @tag :wip + test "fresh user get empty paged published videos", ~m(user)a do + {:ok, results} = Accounts.published_contents(user, :video, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == 0 + end + + @tag :wip + test "user can get paged published videos", ~m(user user2 community community2)a do + pub_videos = + Enum.reduce(1..@publish_count, [], fn _, acc -> + video_attrs = mock_attrs(:video, %{community_id: community.id}) + {:ok, video} = CMS.create_content(community, :video, video_attrs, user) + + acc ++ [video] + end) + + pub_videos2 = + Enum.reduce(1..@publish_count, [], fn _, acc -> + video_attrs = mock_attrs(:video, %{community_id: community2.id}) + {:ok, video} = CMS.create_content(community, :video, video_attrs, user) + + acc ++ [video] + end) + + # unrelated other user + Enum.reduce(1..5, [], fn _, acc -> + video_attrs = mock_attrs(:video, %{community_id: community.id}) + {:ok, video} = CMS.create_content(community, :video, video_attrs, user2) + + acc ++ [video] + end) + + {:ok, results} = Accounts.published_contents(user, :video, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == @publish_count * 2 + + random_video_id = pub_videos |> Enum.random() |> Map.get(:id) + random_video_id2 = pub_videos2 |> Enum.random() |> Map.get(:id) + assert results.entries |> Enum.any?(&(&1.id == random_video_id)) + assert results.entries |> Enum.any?(&(&1.id == random_video_id2)) + end + end + + describe "[Accounts Publised repos]" do + @tag :wip + test "fresh user get empty paged published repos", ~m(user)a do + {:ok, results} = Accounts.published_contents(user, :repo, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == 0 + end + + @tag :wip + test "user can get paged published repos", ~m(user user2 community community2)a do + pub_repos = + Enum.reduce(1..@publish_count, [], fn _, acc -> + repo_attrs = mock_attrs(:repo, %{community_id: community.id}) + {:ok, repo} = CMS.create_content(community, :repo, repo_attrs, user) + + acc ++ [repo] + end) + + pub_repos2 = + Enum.reduce(1..@publish_count, [], fn _, acc -> + repo_attrs = mock_attrs(:repo, %{community_id: community2.id}) + {:ok, repo} = CMS.create_content(community, :repo, repo_attrs, user) + + acc ++ [repo] + end) + + # unrelated other user + Enum.reduce(1..5, [], fn _, acc -> + repo_attrs = mock_attrs(:repo, %{community_id: community.id}) + {:ok, repo} = CMS.create_content(community, :repo, repo_attrs, user2) + + acc ++ [repo] + end) + + {:ok, results} = Accounts.published_contents(user, :repo, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == @publish_count * 2 + + random_repo_id = pub_repos |> Enum.random() |> Map.get(:id) + random_repo_id2 = pub_repos2 |> Enum.random() |> Map.get(:id) + assert results.entries |> Enum.any?(&(&1.id == random_repo_id)) + assert results.entries |> Enum.any?(&(&1.id == random_repo_id2)) + end + end +end diff --git a/test/mastani_server_web/query/accounts/published_contents_test.exs b/test/mastani_server_web/query/accounts/published_contents_test.exs new file mode 100644 index 000000000..deea6feed --- /dev/null +++ b/test/mastani_server_web/query/accounts/published_contents_test.exs @@ -0,0 +1,177 @@ +defmodule MastaniServer.Test.Query.Accounts.PublishedContents do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + @publish_count 10 + + setup do + {:ok, user} = db_insert(:user) + {:ok, community} = db_insert(:community) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(guest_conn user_conn user community)a} + end + + describe "[account favorited posts]" do + @query """ + query($userId: ID!, $filter: PagedFilter!) { + publishedPosts(userId: $userId, filter: $filter) { + entries { + id + author { + id + } + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + @tag :wip + test "user can get paged published posts", ~m(guest_conn user community)a do + pub_posts = + Enum.reduce(1..@publish_count, [], fn _, acc -> + post_attrs = mock_attrs(:post, %{community_id: community.id}) + {:ok, post} = CMS.create_content(community, :post, post_attrs, user) + + acc ++ [post] + end) + + random_post_id = pub_posts |> Enum.random() |> Map.get(:id) |> to_string + + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedPosts") + + assert results |> is_valid_pagination? + assert results["totalCount"] == @publish_count + + assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == random_post_id)) + end + end + + describe "[account favorited jobs]" do + @query """ + query($userId: ID!, $filter: PagedFilter!) { + publishedJobs(userId: $userId, filter: $filter) { + entries { + id + author { + id + } + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + @tag :wip + test "user can get paged published jobs", ~m(guest_conn user community)a do + pub_jobs = + Enum.reduce(1..@publish_count, [], fn _, acc -> + job_attrs = mock_attrs(:job, %{community_id: community.id}) + {:ok, job} = CMS.create_content(community, :job, job_attrs, user) + + acc ++ [job] + end) + + random_job_id = pub_jobs |> Enum.random() |> Map.get(:id) |> to_string + + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedJobs") + + assert results |> is_valid_pagination? + assert results["totalCount"] == @publish_count + + assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == random_job_id)) + end + end + + describe "[account favorited videos]" do + @query """ + query($userId: ID!, $filter: PagedFilter!) { + publishedVideos(userId: $userId, filter: $filter) { + entries { + id + author { + id + } + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + @tag :wip + test "user can get paged published videos", ~m(guest_conn user community)a do + pub_videos = + Enum.reduce(1..@publish_count, [], fn _, acc -> + video_attrs = mock_attrs(:video, %{community_id: community.id}) + {:ok, video} = CMS.create_content(community, :video, video_attrs, user) + + acc ++ [video] + end) + + random_video_id = pub_videos |> Enum.random() |> Map.get(:id) |> to_string + + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedVideos") + + assert results |> is_valid_pagination? + assert results["totalCount"] == @publish_count + + assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == random_video_id)) + end + end + + describe "[account favorited repos]" do + @query """ + query($userId: ID!, $filter: PagedFilter!) { + publishedRepos(userId: $userId, filter: $filter) { + entries { + id + author { + id + } + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + @tag :wip + test "user can get paged published repos", ~m(guest_conn user community)a do + pub_repos = + Enum.reduce(1..@publish_count, [], fn _, acc -> + repo_attrs = mock_attrs(:repo, %{community_id: community.id}) + {:ok, repo} = CMS.create_content(community, :repo, repo_attrs, user) + + acc ++ [repo] + end) + + random_repo_id = pub_repos |> Enum.random() |> Map.get(:id) |> to_string + + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedRepos") + + assert results |> is_valid_pagination? + assert results["totalCount"] == @publish_count + + assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == random_repo_id)) + end + end +end From 7c2c86d775de1edd2b393d70645b40c29ea77b21 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 12 Oct 2018 20:14:12 +0800 Subject: [PATCH 061/129] chore(clean up): rm :wip tag --- test/mastani_server/accounts/published_contents_test.exs | 8 -------- .../query/accounts/achievement_test.exs | 3 --- .../query/accounts/published_contents_test.exs | 4 ---- 3 files changed, 15 deletions(-) diff --git a/test/mastani_server/accounts/published_contents_test.exs b/test/mastani_server/accounts/published_contents_test.exs index d9ffbb3af..db8f08932 100644 --- a/test/mastani_server/accounts/published_contents_test.exs +++ b/test/mastani_server/accounts/published_contents_test.exs @@ -19,7 +19,6 @@ defmodule MastaniServer.Test.Accounts.PublishedContents do end describe "[Accounts Publised posts]" do - @tag :wip test "fresh user get empty paged published posts", ~m(user)a do {:ok, results} = Accounts.published_contents(user, :post, %{page: 1, size: 20}) @@ -27,7 +26,6 @@ defmodule MastaniServer.Test.Accounts.PublishedContents do assert results.total_count == 0 end - @tag :wip test "user can get paged published posts", ~m(user user2 community community2)a do pub_posts = Enum.reduce(1..@publish_count, [], fn _, acc -> @@ -66,7 +64,6 @@ defmodule MastaniServer.Test.Accounts.PublishedContents do end describe "[Accounts Publised jobs]" do - @tag :wip test "fresh user get empty paged published jobs", ~m(user)a do {:ok, results} = Accounts.published_contents(user, :job, %{page: 1, size: 20}) @@ -74,7 +71,6 @@ defmodule MastaniServer.Test.Accounts.PublishedContents do assert results.total_count == 0 end - @tag :wip test "user can get paged published jobs", ~m(user user2 community community2)a do pub_jobs = Enum.reduce(1..@publish_count, [], fn _, acc -> @@ -113,7 +109,6 @@ defmodule MastaniServer.Test.Accounts.PublishedContents do end describe "[Accounts Publised videos]" do - @tag :wip test "fresh user get empty paged published videos", ~m(user)a do {:ok, results} = Accounts.published_contents(user, :video, %{page: 1, size: 20}) @@ -121,7 +116,6 @@ defmodule MastaniServer.Test.Accounts.PublishedContents do assert results.total_count == 0 end - @tag :wip test "user can get paged published videos", ~m(user user2 community community2)a do pub_videos = Enum.reduce(1..@publish_count, [], fn _, acc -> @@ -160,7 +154,6 @@ defmodule MastaniServer.Test.Accounts.PublishedContents do end describe "[Accounts Publised repos]" do - @tag :wip test "fresh user get empty paged published repos", ~m(user)a do {:ok, results} = Accounts.published_contents(user, :repo, %{page: 1, size: 20}) @@ -168,7 +161,6 @@ defmodule MastaniServer.Test.Accounts.PublishedContents do assert results.total_count == 0 end - @tag :wip test "user can get paged published repos", ~m(user user2 community community2)a do pub_repos = Enum.reduce(1..@publish_count, [], fn _, acc -> diff --git a/test/mastani_server_web/query/accounts/achievement_test.exs b/test/mastani_server_web/query/accounts/achievement_test.exs index 3ee54cef0..add960852 100644 --- a/test/mastani_server_web/query/accounts/achievement_test.exs +++ b/test/mastani_server_web/query/accounts/achievement_test.exs @@ -34,7 +34,6 @@ defmodule MastaniServer.Test.Query.Account.Achievement do } } """ - @tag :wip test "can get user's empty editable communities list", ~m(guest_conn user)a do variables = %{userId: user.id, filter: %{page: 1, size: 20}} results = guest_conn |> query_result(@query, variables, "editableCommunities") @@ -42,7 +41,6 @@ defmodule MastaniServer.Test.Query.Account.Achievement do assert results |> is_valid_pagination?(:empty) end - @tag :wip test "can get user's editable communities list when user is editor", ~m(guest_conn user)a do {:ok, community} = db_insert(:community) {:ok, community2} = db_insert(:community) @@ -75,7 +73,6 @@ defmodule MastaniServer.Test.Query.Account.Achievement do } } """ - @tag :wip2 test "user can get own editable communities list", ~m(user)a do user_conn = simu_conn(:user, user) diff --git a/test/mastani_server_web/query/accounts/published_contents_test.exs b/test/mastani_server_web/query/accounts/published_contents_test.exs index deea6feed..ed70533f6 100644 --- a/test/mastani_server_web/query/accounts/published_contents_test.exs +++ b/test/mastani_server_web/query/accounts/published_contents_test.exs @@ -32,7 +32,6 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedContents do } } """ - @tag :wip test "user can get paged published posts", ~m(guest_conn user community)a do pub_posts = Enum.reduce(1..@publish_count, [], fn _, acc -> @@ -72,7 +71,6 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedContents do } } """ - @tag :wip test "user can get paged published jobs", ~m(guest_conn user community)a do pub_jobs = Enum.reduce(1..@publish_count, [], fn _, acc -> @@ -112,7 +110,6 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedContents do } } """ - @tag :wip test "user can get paged published videos", ~m(guest_conn user community)a do pub_videos = Enum.reduce(1..@publish_count, [], fn _, acc -> @@ -152,7 +149,6 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedContents do } } """ - @tag :wip test "user can get paged published repos", ~m(guest_conn user community)a do pub_repos = Enum.reduce(1..@publish_count, [], fn _, acc -> From 4e6accaab073cddfbf503bd8eea9ee405c103b46 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 12 Oct 2018 21:27:20 +0800 Subject: [PATCH 062/129] test(stared contents): add more test on query part --- .../accounts/delegates/reacted_contents.ex | 2 +- lib/mastani_server/accounts/user.ex | 7 + lib/mastani_server/accounts/utils/loader.ex | 14 ++ .../resolvers/accounts_resolver.ex | 9 + .../schema/account/account_queries.ex | 36 ++- .../schema/account/account_types.ex | 51 +++++ .../accounts/reacted_contents_test.exs | 13 +- .../query/accounts/stared_contents_test.exs | 205 ++++++++++++++++++ 8 files changed, 332 insertions(+), 5 deletions(-) create mode 100644 test/mastani_server_web/query/accounts/stared_contents_test.exs diff --git a/lib/mastani_server/accounts/delegates/reacted_contents.ex b/lib/mastani_server/accounts/delegates/reacted_contents.ex index adf5200b1..68de41988 100644 --- a/lib/mastani_server/accounts/delegates/reacted_contents.ex +++ b/lib/mastani_server/accounts/delegates/reacted_contents.ex @@ -28,7 +28,7 @@ defmodule MastaniServer.Accounts.Delegate.ReactedContents do end @doc """ - paged favorite contents + paged favorited/stared contents """ def reacted_contents(thread, react, ~m(page size)a = filter, %User{id: user_id}) do with {:ok, action} <- match_action(thread, react) do diff --git a/lib/mastani_server/accounts/user.ex b/lib/mastani_server/accounts/user.ex index 335a1460e..04af5c274 100644 --- a/lib/mastani_server/accounts/user.ex +++ b/lib/mastani_server/accounts/user.ex @@ -49,6 +49,13 @@ defmodule MastaniServer.Accounts.User do has_many(:followings, {"users_followings", UserFollowing}) has_many(:subscribed_communities, {"communities_subscribers", CMS.CommunitySubscriber}) + + # stared contents + has_many(:stared_posts, {"posts_stars", CMS.PostStar}) + has_many(:stared_jobs, {"jobs_stars", CMS.JobStar}) + has_many(:stared_videos, {"videos_stars", CMS.VideoStar}) + + # favorited contents has_many(:favorited_posts, {"posts_favorites", CMS.PostFavorite}) has_many(:favorited_jobs, {"jobs_favorites", CMS.JobFavorite}) has_many(:favorited_videos, {"videos_favorites", CMS.VideoFavorite}) diff --git a/lib/mastani_server/accounts/utils/loader.ex b/lib/mastani_server/accounts/utils/loader.ex index 13c7c1022..781d490f5 100644 --- a/lib/mastani_server/accounts/utils/loader.ex +++ b/lib/mastani_server/accounts/utils/loader.ex @@ -45,6 +45,20 @@ defmodule MastaniServer.Accounts.Utils.Loader do UserFollower |> where([f], f.follower_id == ^cur_user.id) end + # stared contents count + def query({"posts_stars", CMS.PostStar}, %{count: _}) do + CMS.PostStar |> count_contents + end + + def query({"jobs_stars", CMS.JobStar}, %{count: _}) do + CMS.JobStar |> count_contents + end + + def query({"videos_stars", CMS.VideoStar}, %{count: _}) do + CMS.VideoStar |> count_contents + end + + # favorited contents count def query({"posts_favorites", CMS.PostFavorite}, %{count: _}) do CMS.PostFavorite |> count_contents end diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 87b3067f8..e61dfad1b 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -107,6 +107,15 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.reacted_contents(thread, :favorite, filter, cur_user) end + # gst stared contents + def stared_contents(_root, ~m(user_id filter thread)a, _info) do + Accounts.reacted_contents(thread, :star, filter, %User{id: user_id}) + end + + def stared_contents(_root, ~m(filter thread)a, %{context: %{cur_user: cur_user}}) do + Accounts.reacted_contents(thread, :star, filter, cur_user) + end + # published contents def published_contents(_root, ~m(user_id filter thread)a, _info) do Accounts.published_contents(%User{id: user_id}, thread, filter) diff --git a/lib/mastani_server_web/schema/account/account_queries.ex b/lib/mastani_server_web/schema/account/account_queries.ex index c43f904e0..67c540142 100644 --- a/lib/mastani_server_web/schema/account/account_queries.ex +++ b/lib/mastani_server_web/schema/account/account_queries.ex @@ -68,9 +68,39 @@ defmodule MastaniServerWeb.Schema.Account.Queries do resolve(&R.Accounts.list_favorite_categories/3) end + @doc "paged stared posts" + field :stared_posts, :paged_posts do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:thread, :post_thread, default_value: :post) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.stared_contents/3) + end + + @doc "paged stared jobs" + field :stared_jobs, :paged_jobs do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:thread, :job_thread, default_value: :job) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.stared_contents/3) + end + + @doc "paged stared videos" + field :stared_videos, :paged_videos do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:thread, :video_thread, default_value: :video) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.stared_contents/3) + end + @desc "get favorited posts" field :favorited_posts, :paged_posts do - arg(:user_id, :id) + arg(:user_id, non_null(:id)) arg(:filter, non_null(:paged_filter)) arg(:category_id, :id) arg(:thread, :post_thread, default_value: :post) @@ -81,7 +111,7 @@ defmodule MastaniServerWeb.Schema.Account.Queries do @desc "get favorited jobs" field :favorited_jobs, :paged_jobs do - arg(:user_id, :id) + arg(:user_id, non_null(:id)) arg(:filter, non_null(:paged_filter)) arg(:category_id, :id) arg(:thread, :job_thread, default_value: :job) @@ -92,7 +122,7 @@ defmodule MastaniServerWeb.Schema.Account.Queries do @desc "get favorited jobs" field :favorited_videos, :paged_videos do - arg(:user_id, :id) + arg(:user_id, non_null(:id)) arg(:filter, non_null(:paged_filter)) arg(:category_id, :id) arg(:thread, :video_thread, default_value: :video) diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index 6679100a8..b08a07a2f 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -120,6 +120,33 @@ defmodule MastaniServerWeb.Schema.Account.Types do middleware(M.ViewerDidConvert) end + @doc "paged stared posts" + field :stared_posts, :paged_posts do + arg(:filter, non_null(:paged_filter)) + arg(:thread, :post_thread, default_value: :post) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.stared_contents/3) + end + + @doc "paged stared jobs" + field :stared_jobs, :paged_jobs do + arg(:filter, non_null(:paged_filter)) + arg(:thread, :job_thread, default_value: :job) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.stared_contents/3) + end + + @doc "paged stared videos" + field :stared_videos, :paged_videos do + arg(:filter, non_null(:paged_filter)) + arg(:thread, :video_thread, default_value: :video) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.stared_contents/3) + end + @doc "paged favorited posts" field :favorited_posts, :paged_posts do arg(:filter, non_null(:paged_filter)) @@ -147,6 +174,30 @@ defmodule MastaniServerWeb.Schema.Account.Types do resolve(&R.Accounts.favorited_contents/3) end + @doc "total count of stared posts count" + field :stared_posts_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(Accounts, :stared_posts)) + middleware(M.ConvertToInt) + end + + @doc "total count of stared jobs count" + field :stared_jobs_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(Accounts, :stared_jobs)) + middleware(M.ConvertToInt) + end + + @doc "total count of stared videos count" + field :stared_videos_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(Accounts, :stared_videos)) + middleware(M.ConvertToInt) + end + @doc "total count of favorited posts count" field :favorited_posts_count, :integer do arg(:count, :count_type, default_value: :count) diff --git a/test/mastani_server/accounts/reacted_contents_test.exs b/test/mastani_server/accounts/reacted_contents_test.exs index a9b8a2dd8..8ebf3bb9d 100644 --- a/test/mastani_server/accounts/reacted_contents_test.exs +++ b/test/mastani_server/accounts/reacted_contents_test.exs @@ -7,8 +7,9 @@ defmodule MastaniServer.Test.Accounts.ReactedContents do {:ok, user} = db_insert(:user) {:ok, post} = db_insert(:post) {:ok, job} = db_insert(:job) + {:ok, video} = db_insert(:video) - {:ok, ~m(user post job)a} + {:ok, ~m(user post job video)a} end describe "[user favorited contents]" do @@ -49,5 +50,15 @@ defmodule MastaniServer.Test.Accounts.ReactedContents do assert jobs |> is_valid_pagination?(:raw) assert job.id == jobs |> Map.get(:entries) |> List.first() |> Map.get(:id) end + + @tag :wip + test "user can get paged stared_videos", ~m(user video)a do + {:ok, _} = CMS.reaction(:video, :star, video.id, user) + + filter = %{page: 1, size: 20} + {:ok, jobs} = Accounts.reacted_contents(:video, :star, filter, user) + assert jobs |> is_valid_pagination?(:raw) + assert video.id == jobs |> Map.get(:entries) |> List.first() |> Map.get(:id) + end end end diff --git a/test/mastani_server_web/query/accounts/stared_contents_test.exs b/test/mastani_server_web/query/accounts/stared_contents_test.exs new file mode 100644 index 000000000..e17d3698e --- /dev/null +++ b/test/mastani_server_web/query/accounts/stared_contents_test.exs @@ -0,0 +1,205 @@ +defmodule MastaniServer.Test.Query.Accounts.StaredContents do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + @total_count 20 + + setup do + {:ok, user} = db_insert(:user) + {:ok, posts} = db_insert_multi(:post, @total_count) + {:ok, jobs} = db_insert_multi(:job, @total_count) + {:ok, videos} = db_insert_multi(:video, @total_count) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(guest_conn user_conn user posts jobs videos)a} + end + + describe "[accounts stared posts]" do + @query """ + query($filter: PagedFilter!) { + account { + id + staredPosts(filter: $filter) { + entries { + id + } + totalCount + } + staredPostsCount + } + } + """ + @tag :wip + test "login user can get it's own staredPosts", ~m(user_conn user posts)a do + Enum.each(posts, fn post -> + {:ok, _} = CMS.reaction(:post, :star, post.id, user) + end) + + random_id = posts |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string + + variables = %{filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "account") + # IO.inspect results, label: "hello" + assert results["staredPosts"] |> Map.get("totalCount") == @total_count + assert results["staredPostsCount"] == @total_count + + assert results["staredPosts"] + |> Map.get("entries") + |> Enum.any?(&(&1["id"] == random_id)) + end + + @query """ + query($userId: ID, $filter: PagedFilter!) { + staredPosts(userId: $userId, filter: $filter) { + entries { + id + } + totalCount + } + } + """ + @tag :wip + test "other user can get other user's paged staredPosts", + ~m(user_conn guest_conn posts)a do + {:ok, user} = db_insert(:user) + + Enum.each(posts, fn post -> + {:ok, _} = CMS.reaction(:post, :star, post.id, user) + end) + + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "staredPosts") + results2 = guest_conn |> query_result(@query, variables, "staredPosts") + + assert results["totalCount"] == @total_count + assert results2["totalCount"] == @total_count + end + end + + describe "[accounts stared jobs]" do + @query """ + query($filter: PagedFilter!) { + account { + id + staredJobs(filter: $filter) { + entries { + id + } + totalCount + } + staredJobsCount + } + } + """ + @tag :wip + test "login user can get it's own staredJobs", ~m(user_conn user jobs)a do + Enum.each(jobs, fn job -> + {:ok, _} = CMS.reaction(:job, :star, job.id, user) + end) + + random_id = jobs |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string + + variables = %{filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "account") + # IO.inspect results, label: "hello" + assert results["staredJobs"] |> Map.get("totalCount") == @total_count + assert results["staredJobsCount"] == @total_count + + assert results["staredJobs"] + |> Map.get("entries") + |> Enum.any?(&(&1["id"] == random_id)) + end + + @query """ + query($userId: ID, $filter: PagedFilter!) { + staredJobs(userId: $userId, filter: $filter) { + entries { + id + } + totalCount + } + } + """ + @tag :wip + test "other user can get other user's paged staredJobs", + ~m(user_conn guest_conn jobs)a do + {:ok, user} = db_insert(:user) + + Enum.each(jobs, fn job -> + {:ok, _} = CMS.reaction(:job, :star, job.id, user) + end) + + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "staredJobs") + results2 = guest_conn |> query_result(@query, variables, "staredJobs") + + assert results["totalCount"] == @total_count + assert results2["totalCount"] == @total_count + end + end + + describe "[accounts stared videos]" do + @query """ + query($filter: PagedFilter!) { + account { + id + staredVideos(filter: $filter) { + entries { + id + } + totalCount + } + staredVideosCount + } + } + """ + @tag :wip + test "login user can get it's own staredVideos", ~m(user_conn user videos)a do + Enum.each(videos, fn video -> + {:ok, _} = CMS.reaction(:video, :star, video.id, user) + end) + + random_id = videos |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string + + variables = %{filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "account") + # IO.inspect results, label: "hello" + assert results["staredVideos"] |> Map.get("totalCount") == @total_count + assert results["staredVideosCount"] == @total_count + + assert results["staredVideos"] + |> Map.get("entries") + |> Enum.any?(&(&1["id"] == random_id)) + end + + @query """ + query($userId: ID, $filter: PagedFilter!) { + staredVideos(userId: $userId, filter: $filter) { + entries { + id + } + totalCount + } + } + """ + @tag :wip + test "other user can get other user's paged staredVideos", + ~m(user_conn guest_conn videos)a do + {:ok, user} = db_insert(:user) + + Enum.each(videos, fn video -> + {:ok, _} = CMS.reaction(:video, :star, video.id, user) + end) + + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "staredVideos") + results2 = guest_conn |> query_result(@query, variables, "staredVideos") + + assert results["totalCount"] == @total_count + assert results2["totalCount"] == @total_count + end + end +end From 8c2e339dfa9860875ed09914a71b058b214fbb18 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 12 Oct 2018 21:37:56 +0800 Subject: [PATCH 063/129] refactor(uer favorites test): extract total_count -> @total_count --- .../query/accounts/favorited_jobs_test.exs | 28 ++++++++-------- .../query/accounts/favorited_posts_test.exs | 32 +++++++----------- .../query/accounts/favorited_videos_test.exs | 33 +++++++------------ 3 files changed, 37 insertions(+), 56 deletions(-) diff --git a/test/mastani_server_web/query/accounts/favorited_jobs_test.exs b/test/mastani_server_web/query/accounts/favorited_jobs_test.exs index a446f47ab..4388afc0d 100644 --- a/test/mastani_server_web/query/accounts/favorited_jobs_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_jobs_test.exs @@ -3,15 +3,16 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do alias MastaniServer.CMS + @total_count 20 + setup do {:ok, user} = db_insert(:user) - total_count = 20 - {:ok, jobs} = db_insert_multi(:job, total_count) + {:ok, jobs} = db_insert_multi(:job, @total_count) guest_conn = simu_conn(:guest) user_conn = simu_conn(:user, user) - {:ok, ~m(guest_conn user_conn user total_count jobs)a} + {:ok, ~m(guest_conn user_conn user jobs)a} end describe "[accounts favorited jobs]" do @@ -29,7 +30,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do } } """ - test "login user can get it's own favoritedJobs", ~m(user_conn user total_count jobs)a do + test "login user can get it's own favoritedJobs", ~m(user_conn user jobs)a do Enum.each(jobs, fn job -> {:ok, _} = CMS.reaction(:job, :favorite, job.id, user) end) @@ -39,8 +40,8 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do variables = %{filter: %{page: 1, size: 20}} results = user_conn |> query_result(@query, variables, "account") # IO.inspect results, label: "hello" - assert results["favoritedJobs"] |> Map.get("totalCount") == total_count - assert results["favoritedJobsCount"] == total_count + assert results["favoritedJobs"] |> Map.get("totalCount") == @total_count + assert results["favoritedJobsCount"] == @total_count assert results["favoritedJobs"] |> Map.get("entries") @@ -48,7 +49,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do end @query """ - query($userId: ID, $categoryId: ID,$filter: PagedFilter!) { + query($userId: ID!, $categoryId: ID,$filter: PagedFilter!) { favoritedJobs(userId: $userId, categoryId: $categoryId, filter: $filter) { entries { id @@ -58,7 +59,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do } """ test "other user can get other user's paged favoritedJobs", - ~m(user_conn guest_conn total_count jobs)a do + ~m(user_conn guest_conn jobs)a do {:ok, user} = db_insert(:user) Enum.each(jobs, fn job -> @@ -69,19 +70,20 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do results = user_conn |> query_result(@query, variables, "favoritedJobs") results2 = guest_conn |> query_result(@query, variables, "favoritedJobs") - assert results["totalCount"] == total_count - assert results2["totalCount"] == total_count + assert results["totalCount"] == @total_count + assert results2["totalCount"] == @total_count end - test "login user can get self paged favoritedJobs", ~m(user_conn user total_count jobs)a do + @tag :wip2 + test "login user can get self paged favoritedJobs", ~m(user_conn user jobs)a do Enum.each(jobs, fn job -> {:ok, _} = CMS.reaction(:job, :favorite, job.id, user) end) - variables = %{filter: %{page: 1, size: 20}} + variables = %{userId: user.id, filter: %{page: 1, size: 20}} results = user_conn |> query_result(@query, variables, "favoritedJobs") - assert results["totalCount"] == total_count + assert results["totalCount"] == @total_count end alias MastaniServer.Accounts diff --git a/test/mastani_server_web/query/accounts/favorited_posts_test.exs b/test/mastani_server_web/query/accounts/favorited_posts_test.exs index 658eb3028..815c748e2 100644 --- a/test/mastani_server_web/query/accounts/favorited_posts_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_posts_test.exs @@ -3,16 +3,17 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedPosts do alias MastaniServer.CMS + @total_count 20 + setup do {:ok, user} = db_insert(:user) - total_count = 20 - {:ok, posts} = db_insert_multi(:post, total_count) + {:ok, posts} = db_insert_multi(:post, @total_count) guest_conn = simu_conn(:guest) user_conn = simu_conn(:user, user) - {:ok, ~m(guest_conn user_conn user total_count posts)a} + {:ok, ~m(guest_conn user_conn user posts)a} end describe "[account favorited posts]" do @@ -30,7 +31,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedPosts do } } """ - test "login user can get it's own favoritedPosts", ~m(user_conn user total_count posts)a do + test "login user can get it's own favoritedPosts", ~m(user_conn user posts)a do Enum.each(posts, fn post -> {:ok, _} = CMS.reaction(:post, :favorite, post.id, user) end) @@ -39,8 +40,8 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedPosts do variables = %{filter: %{page: 1, size: 20}} results = user_conn |> query_result(@query, variables, "account") - assert results["favoritedPosts"] |> Map.get("totalCount") == total_count - assert results["favoritedPostsCount"] == total_count + assert results["favoritedPosts"] |> Map.get("totalCount") == @total_count + assert results["favoritedPostsCount"] == @total_count assert results["favoritedPosts"] |> Map.get("entries") @@ -48,7 +49,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedPosts do end @query """ - query($userId: ID, $categoryId: ID, $filter: PagedFilter!) { + query($userId: ID!, $categoryId: ID, $filter: PagedFilter!) { favoritedPosts(userId: $userId, categoryId: $categoryId, filter: $filter) { entries { id @@ -58,7 +59,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedPosts do } """ test "other user can get other user's paged favoritedPosts", - ~m(user_conn guest_conn total_count posts)a do + ~m(user_conn guest_conn posts)a do {:ok, user} = db_insert(:user) Enum.each(posts, fn post -> @@ -69,19 +70,8 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedPosts do results = user_conn |> query_result(@query, variables, "favoritedPosts") results2 = guest_conn |> query_result(@query, variables, "favoritedPosts") - assert results["totalCount"] == total_count - assert results2["totalCount"] == total_count - end - - test "login user can get self paged favoritedPosts", ~m(user_conn user posts total_count)a do - Enum.each(posts, fn post -> - {:ok, _} = CMS.reaction(:post, :favorite, post.id, user) - end) - - variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "favoritedPosts") - - assert results["totalCount"] == total_count + assert results["totalCount"] == @total_count + assert results2["totalCount"] == @total_count end alias MastaniServer.Accounts diff --git a/test/mastani_server_web/query/accounts/favorited_videos_test.exs b/test/mastani_server_web/query/accounts/favorited_videos_test.exs index b3f17bdb4..a55f87bc4 100644 --- a/test/mastani_server_web/query/accounts/favorited_videos_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_videos_test.exs @@ -3,16 +3,17 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedVideos do alias MastaniServer.CMS + @total_count 20 + setup do {:ok, user} = db_insert(:user) - total_count = 20 - {:ok, videos} = db_insert_multi(:video, total_count) + {:ok, videos} = db_insert_multi(:video, @total_count) guest_conn = simu_conn(:guest) user_conn = simu_conn(:user, user) - {:ok, ~m(guest_conn user_conn user total_count videos)a} + {:ok, ~m(guest_conn user_conn user videos)a} end describe "[account favorited videos]" do @@ -30,7 +31,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedVideos do } } """ - test "login user can get it's own favoritedVideos", ~m(user_conn user total_count videos)a do + test "login user can get it's own favoritedVideos", ~m(user_conn user videos)a do Enum.each(videos, fn video -> {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) end) @@ -39,8 +40,8 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedVideos do variables = %{filter: %{page: 1, size: 20}} results = user_conn |> query_result(@query, variables, "account") - assert results["favoritedVideos"] |> Map.get("totalCount") == total_count - assert results["favoritedVideosCount"] == total_count + assert results["favoritedVideos"] |> Map.get("totalCount") == @total_count + assert results["favoritedVideosCount"] == @total_count assert results["favoritedVideos"] |> Map.get("entries") @@ -48,7 +49,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedVideos do end @query """ - query($userId: ID, $categoryId: ID, $filter: PagedFilter!) { + query($userId: ID!, $categoryId: ID, $filter: PagedFilter!) { favoritedVideos(userId: $userId, categoryId: $categoryId, filter: $filter) { entries { id @@ -58,7 +59,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedVideos do } """ test "other user can get other user's paged favoritedVideos", - ~m(user_conn guest_conn total_count videos)a do + ~m(user_conn guest_conn videos)a do {:ok, user} = db_insert(:user) Enum.each(videos, fn video -> @@ -69,20 +70,8 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedVideos do results = user_conn |> query_result(@query, variables, "favoritedVideos") results2 = guest_conn |> query_result(@query, variables, "favoritedVideos") - assert results["totalCount"] == total_count - assert results2["totalCount"] == total_count - end - - test "login user can get self paged favoritedVideos", - ~m(user_conn user videos total_count)a do - Enum.each(videos, fn video -> - {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) - end) - - variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "favoritedVideos") - - assert results["totalCount"] == total_count + assert results["totalCount"] == @total_count + assert results2["totalCount"] == @total_count end alias MastaniServer.Accounts From 029d5024c738d87a00a13da5aec65db82baab04f Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 12 Oct 2018 21:40:14 +0800 Subject: [PATCH 064/129] chore(tests): clean warnings & rm :wip tag --- test/mastani_server/accounts/published_contents_test.exs | 4 ---- test/mastani_server/accounts/reacted_contents_test.exs | 1 - .../query/accounts/favorited_jobs_test.exs | 1 - .../query/accounts/stared_contents_test.exs | 6 ------ 4 files changed, 12 deletions(-) diff --git a/test/mastani_server/accounts/published_contents_test.exs b/test/mastani_server/accounts/published_contents_test.exs index db8f08932..b1105a78c 100644 --- a/test/mastani_server/accounts/published_contents_test.exs +++ b/test/mastani_server/accounts/published_contents_test.exs @@ -1,11 +1,7 @@ defmodule MastaniServer.Test.Accounts.PublishedContents do use MastaniServer.TestTools - import Helper.Utils, only: [get_config: 2] - alias Helper.ORM - alias MastaniServer.{Accounts, CMS} - alias MastaniServer.Accounts.User @publish_count 10 diff --git a/test/mastani_server/accounts/reacted_contents_test.exs b/test/mastani_server/accounts/reacted_contents_test.exs index 8ebf3bb9d..72a855047 100644 --- a/test/mastani_server/accounts/reacted_contents_test.exs +++ b/test/mastani_server/accounts/reacted_contents_test.exs @@ -51,7 +51,6 @@ defmodule MastaniServer.Test.Accounts.ReactedContents do assert job.id == jobs |> Map.get(:entries) |> List.first() |> Map.get(:id) end - @tag :wip test "user can get paged stared_videos", ~m(user video)a do {:ok, _} = CMS.reaction(:video, :star, video.id, user) diff --git a/test/mastani_server_web/query/accounts/favorited_jobs_test.exs b/test/mastani_server_web/query/accounts/favorited_jobs_test.exs index 4388afc0d..ce4008ed8 100644 --- a/test/mastani_server_web/query/accounts/favorited_jobs_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_jobs_test.exs @@ -74,7 +74,6 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do assert results2["totalCount"] == @total_count end - @tag :wip2 test "login user can get self paged favoritedJobs", ~m(user_conn user jobs)a do Enum.each(jobs, fn job -> {:ok, _} = CMS.reaction(:job, :favorite, job.id, user) diff --git a/test/mastani_server_web/query/accounts/stared_contents_test.exs b/test/mastani_server_web/query/accounts/stared_contents_test.exs index e17d3698e..7bd86b038 100644 --- a/test/mastani_server_web/query/accounts/stared_contents_test.exs +++ b/test/mastani_server_web/query/accounts/stared_contents_test.exs @@ -32,7 +32,6 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do } } """ - @tag :wip test "login user can get it's own staredPosts", ~m(user_conn user posts)a do Enum.each(posts, fn post -> {:ok, _} = CMS.reaction(:post, :star, post.id, user) @@ -61,7 +60,6 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do } } """ - @tag :wip test "other user can get other user's paged staredPosts", ~m(user_conn guest_conn posts)a do {:ok, user} = db_insert(:user) @@ -94,7 +92,6 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do } } """ - @tag :wip test "login user can get it's own staredJobs", ~m(user_conn user jobs)a do Enum.each(jobs, fn job -> {:ok, _} = CMS.reaction(:job, :star, job.id, user) @@ -123,7 +120,6 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do } } """ - @tag :wip test "other user can get other user's paged staredJobs", ~m(user_conn guest_conn jobs)a do {:ok, user} = db_insert(:user) @@ -156,7 +152,6 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do } } """ - @tag :wip test "login user can get it's own staredVideos", ~m(user_conn user videos)a do Enum.each(videos, fn video -> {:ok, _} = CMS.reaction(:video, :star, video.id, user) @@ -185,7 +180,6 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do } } """ - @tag :wip test "other user can get other user's paged staredVideos", ~m(user_conn guest_conn videos)a do {:ok, user} = db_insert(:user) From 1f818cf23d36aca72f07465be562fcfab2181685 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 14 Oct 2018 10:32:51 +0800 Subject: [PATCH 065/129] test(thread comments): add test for particlepators --- .../query/cms/post_comment_test.exs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/mastani_server_web/query/cms/post_comment_test.exs b/test/mastani_server_web/query/cms/post_comment_test.exs index 804f57c84..9c3a2c7e5 100644 --- a/test/mastani_server_web/query/cms/post_comment_test.exs +++ b/test/mastani_server_web/query/cms/post_comment_test.exs @@ -13,6 +13,63 @@ defmodule MastaniServer.Test.Query.PostComment do {:ok, ~m(user_conn guest_conn post user)a} end + describe "[post dataloader comment]" do + @query """ + query($filter: PagedArticleFilter) { + pagedPosts(filter: $filter) { + entries { + id + title + commentsParticipatorsCount + commentsParticipators(filter: { first: 5 }) { + id + nickname + } + commentsCount + } + totalCount + } + } + """ + test "can get comments participators of a post", ~m(user guest_conn)a do + {:ok, user2} = db_insert(:user) + + {:ok, community} = db_insert(:community) + {:ok, post} = CMS.create_content(community, :post, mock_attrs(:post), user) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedPosts") + + comments_participators_count = + results["entries"] |> List.first() |> Map.get("commentsParticipatorsCount") + + assert comments_participators_count == 0 + + body = "this is a test comment" + assert {:ok, _comment} = CMS.create_comment(:post, post.id, body, user) + assert {:ok, _comment} = CMS.create_comment(:post, post.id, body, user) + + assert {:ok, _comment} = CMS.create_comment(:post, post.id, body, user2) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedPosts") + + comments_participators_count = + results["entries"] |> List.first() |> Map.get("commentsParticipatorsCount") + + comments_count = results["entries"] |> List.first() |> Map.get("commentsCount") + + assert comments_participators_count == 2 + assert comments_count == 3 + + comments_participators = + results["entries"] |> List.first() |> Map.get("commentsParticipators") + + assert comments_participators |> Enum.any?(&(&1["id"] == to_string(user.id))) + assert comments_participators |> Enum.any?(&(&1["id"] == to_string(user2.id))) + end + end + # TODO: user can get specific user's replies :list_replies # TODO: filter comment by time / like / reply describe "[post comment]" do From 40fea00e13bdcb363a70b2cf2bd594377beea870 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 14 Oct 2018 17:11:16 +0800 Subject: [PATCH 066/129] feat(cms.contents): add favroted_category_id query field --- .../accounts/delegates/publish.ex | 8 ++-- lib/mastani_server/cms/cms.ex | 2 + .../cms/delegates/favorited_category.ex | 40 +++++++++++++++++++ .../resolvers/accounts_resolver.ex | 8 ++-- .../resolvers/cms_resolver.ex | 4 ++ lib/mastani_server_web/schema/utils/helper.ex | 16 ++++++++ .../query/cms/post_test.exs | 35 ++++++++++++++++ .../query/cms/video_test.exs | 35 ++++++++++++++++ 8 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 lib/mastani_server/cms/delegates/favorited_category.ex diff --git a/lib/mastani_server/accounts/delegates/publish.ex b/lib/mastani_server/accounts/delegates/publish.ex index 4e9054c29..7385cf7b2 100644 --- a/lib/mastani_server/accounts/delegates/publish.ex +++ b/lib/mastani_server/accounts/delegates/publish.ex @@ -4,15 +4,15 @@ defmodule MastaniServer.Accounts.Delegate.Publish do """ import Ecto.Query, warn: false import Helper.Utils, only: [done: 1] - import Helper.ErrorCode + # import Helper.ErrorCode import ShortMaps import MastaniServer.CMS.Utils.Matcher - alias Helper.{ORM, QueryBuilder, SpecType} - alias MastaniServer.{Accounts, Repo} + alias Helper.{ORM, QueryBuilder} + # alias MastaniServer.{Accounts, Repo} - alias MastaniServer.Accounts.{User} + alias MastaniServer.Accounts.User alias MastaniServer.CMS @doc """ diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index 23d229220..c8624f3ca 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -8,6 +8,7 @@ defmodule MastaniServer.CMS do ArticleCURD, ArticleOperation, ArticleReaction, + FavoritedContents, CommentCURD, CommunitySync, CommentReaction, @@ -67,6 +68,7 @@ defmodule MastaniServer.CMS do defdelegate reaction(thread, react, content_id, user), to: ArticleReaction defdelegate undo_reaction(thread, react, content_id, user), to: ArticleReaction + defdelegate favorited_category(thread, content_id, user), to: FavoritedContents # ArticleOperation # >> set flag on article, like: pin / unpin article defdelegate set_community_flags(queryable, community_id, attrs), to: ArticleOperation diff --git a/lib/mastani_server/cms/delegates/favorited_category.ex b/lib/mastani_server/cms/delegates/favorited_category.ex new file mode 100644 index 000000000..10a490b6b --- /dev/null +++ b/lib/mastani_server/cms/delegates/favorited_category.ex @@ -0,0 +1,40 @@ +defmodule MastaniServer.CMS.Delegate.FavoritedContents do + @moduledoc """ + CURD operation on post/job/video ... + """ + alias Helper.ORM + + import Ecto.Query, warn: false + alias MastaniServer.Accounts.User + alias MastaniServer.CMS + + def favorited_category(:post, id, %User{id: user_id}) do + case ORM.find_by(CMS.PostFavorite, post_id: id, user_id: user_id) do + {:ok, post_favorite} -> + {:ok, post_favorite.category_id} + + _ -> + {:ok, nil} + end + end + + def favorited_category(:job, id, %User{id: user_id}) do + case ORM.find_by(CMS.JobFavorite, job_id: id, user_id: user_id) do + {:ok, job_favorite} -> + {:ok, job_favorite.category_id} + + _ -> + {:ok, nil} + end + end + + def favorited_category(:video, id, %User{id: user_id}) do + case ORM.find_by(CMS.VideoFavorite, video_id: id, user_id: user_id) do + {:ok, video_favorite} -> + {:ok, video_favorite.category_id} + + _ -> + {:ok, nil} + end + end +end diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index e61dfad1b..08f9a45ef 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -130,14 +130,14 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.list_editable_communities(%User{id: user_id}, filter) end - # def editable_communities(root, ~m(filter)a, _info) do - # Accounts.list_editable_communities(%User{id: root.id}, filter) - # end - def editable_communities(_root, ~m(filter)a, %{context: %{cur_user: cur_user}}) do Accounts.list_editable_communities(cur_user, filter) end + def editable_communities(root, ~m(filter)a, _info) do + Accounts.list_editable_communities(%User{id: root.id}, filter) + end + # TODO: refactor def get_mail_box_status(_root, _args, %{context: %{cur_user: cur_user}}) do Accounts.mailbox_status(cur_user) diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 7756f6913..4026bc6fd 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -92,6 +92,10 @@ defmodule MastaniServerWeb.Resolvers.CMS do CMS.reaction_users(thread, action, id, filter) end + def favorited_category(root, ~m(thread)a, %{context: %{cur_user: user}}) do + CMS.favorited_category(thread, root.id, user) + end + # ####################### # category .. # ####################### diff --git a/lib/mastani_server_web/schema/utils/helper.ex b/lib/mastani_server_web/schema/utils/helper.ex index 154993ca4..0746db6db 100644 --- a/lib/mastani_server_web/schema/utils/helper.ex +++ b/lib/mastani_server_web/schema/utils/helper.ex @@ -50,11 +50,13 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do import Absinthe.Resolution.Helpers, only: [dataloader: 2] alias MastaniServer.CMS + alias MastaniServerWeb.Resolvers, as: R alias MastaniServerWeb.Middleware, as: M # fields for: favorite count, users, viewer_did_favorite.. defmacro favorite_fields(thread) do quote do + @doc "if viewer has favroted of this #{unquote(thread)}" field :viewer_has_favorited, :boolean do arg(:viewer_did, :viewer_did_type, default_value: :viewer_did) @@ -64,6 +66,7 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do middleware(M.ViewerDidConvert) end + @doc "favroted count of this #{unquote(thread)}" field :favorited_count, :integer do arg(:count, :count_type, default_value: :count) @@ -77,12 +80,25 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do middleware(M.ConvertToInt) end + @doc "list of user who has favroted this #{unquote(thread)}" field :favorited_users, list_of(:user) do arg(:filter, :members_filter) middleware(M.PageSizeProof) resolve(dataloader(CMS, :favorites)) end + + @doc "get viewer's favroted category if seted" + field :favorited_category_id, :id do + arg( + :thread, + unquote(String.to_atom("#{to_string(thread)}_thread")), + default_value: unquote(thread) + ) + + middleware(M.Authorize, :login) + resolve(&R.CMS.favorited_category/3) + end end end diff --git a/test/mastani_server_web/query/cms/post_test.exs b/test/mastani_server_web/query/cms/post_test.exs index b3f5cbb4c..98ac2279f 100644 --- a/test/mastani_server_web/query/cms/post_test.exs +++ b/test/mastani_server_web/query/cms/post_test.exs @@ -119,4 +119,39 @@ defmodule MastaniServer.Test.Query.Post do variables = %{id: post.id} assert guest_conn |> query_get_error?(@query, variables, ecode(:account_login)) end + + alias MastaniServer.Accounts + + @query """ + query($id: ID!) { + post(id: $id) { + id + favoritedCategoryId + } + } + """ + @tag :wip + test "login user can get nil post favorited category id", ~m(post)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + variables = %{id: post.id} + result = user_conn |> query_result(@query, variables, "post") + assert result["favoritedCategoryId"] == nil + end + + @tag :wip + test "login user can get post favorited category id after favorited", ~m(post)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, _favorite_category} = Accounts.set_favorites(user, :post, post.id, category.id) + + variables = %{id: post.id} + result = user_conn |> query_result(@query, variables, "post") + + assert result["favoritedCategoryId"] == to_string(category.id) + end end diff --git a/test/mastani_server_web/query/cms/video_test.exs b/test/mastani_server_web/query/cms/video_test.exs index 82a38da88..5f27ee776 100644 --- a/test/mastani_server_web/query/cms/video_test.exs +++ b/test/mastani_server_web/query/cms/video_test.exs @@ -41,4 +41,39 @@ defmodule MastaniServer.Test.Query.Video do views_2 = user_conn |> query_result(@query, variables, "video") |> Map.get("views") assert views_2 == views_1 + 1 end + + alias MastaniServer.Accounts + + @query """ + query($id: ID!) { + video(id: $id) { + id + favoritedCategoryId + } + } + """ + @tag :wip + test "login user can get nil video favorited category id", ~m(video)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + variables = %{id: video.id} + result = user_conn |> query_result(@query, variables, "video") + assert result["favoritedCategoryId"] == nil + end + + @tag :wip + test "login user can get video favorited category id after favorited", ~m(video)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, _favorite_category} = Accounts.set_favorites(user, :video, video.id, category.id) + + variables = %{id: video.id} + result = user_conn |> query_result(@query, variables, "video") + + assert result["favoritedCategoryId"] == to_string(category.id) + end end From 3f499344ed20833c00682ee18782fe15920b9285 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 14 Oct 2018 17:21:54 +0800 Subject: [PATCH 067/129] test: restore some job tests & clean up --- .../accounts/delegates/publish.ex | 2 +- .../cms/delegates/article_reaction.ex | 4 +- lib/mastani_server/cms/utils/loader.ex | 4 +- .../mastani_server_web/query/cms/job_test.exs | 172 +++++++++--------- .../query/cms/post_test.exs | 2 - .../query/cms/video_test.exs | 2 - 6 files changed, 96 insertions(+), 90 deletions(-) diff --git a/lib/mastani_server/accounts/delegates/publish.ex b/lib/mastani_server/accounts/delegates/publish.ex index 7385cf7b2..32641d87b 100644 --- a/lib/mastani_server/accounts/delegates/publish.ex +++ b/lib/mastani_server/accounts/delegates/publish.ex @@ -13,7 +13,7 @@ defmodule MastaniServer.Accounts.Delegate.Publish do # alias MastaniServer.{Accounts, Repo} alias MastaniServer.Accounts.User - alias MastaniServer.CMS + # alias MastaniServer.CMS @doc """ get paged published contets of a user diff --git a/lib/mastani_server/cms/delegates/article_reaction.ex b/lib/mastani_server/cms/delegates/article_reaction.ex index 218808d50..ee0713bb4 100644 --- a/lib/mastani_server/cms/delegates/article_reaction.ex +++ b/lib/mastani_server/cms/delegates/article_reaction.ex @@ -36,7 +36,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleReaction do defp reaction_result({:ok, %{create_reaction_record: result}}), do: result |> done() - defp reaction_result({:error, :create_reaction_record, result, _steps}) do + defp reaction_result({:error, :create_reaction_record, _result, _steps}) do {:error, [message: "create reaction fails", code: ecode(:react_fails)]} end @@ -75,7 +75,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleReaction do defp undo_reaction_result({:ok, %{delete_reaction_record: result}}), do: result |> done() - defp undo_reaction_result({:error, :delete_reaction_record, result, _steps}) do + defp undo_reaction_result({:error, :delete_reaction_record, _result, _steps}) do {:error, [message: "delete reaction fails", code: ecode(:react_fails)]} end diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index b2350af5e..d893f278f 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -21,9 +21,9 @@ defmodule MastaniServer.CMS.Utils.Loader do PostFavorite, PostStar, # JOB - Job, + # Job, JobFavorite, - JobStar, + # JobStar, JobCommentReply, JobCommentDislike, JobCommentLike, diff --git a/test/mastani_server_web/query/cms/job_test.exs b/test/mastani_server_web/query/cms/job_test.exs index 9ecc46230..7e6536928 100644 --- a/test/mastani_server_web/query/cms/job_test.exs +++ b/test/mastani_server_web/query/cms/job_test.exs @@ -38,85 +38,95 @@ defmodule MastaniServer.Test.Query.Job do assert is_valid_kv?(results, "body", :string) end - # @query """ - # query($id: ID!) { - # job(id: $id) { - # id - # favoritedUsers { - # nickname - # id - # } - # } - # } - # """ - # test "post have favoritedUsers query field", ~m(user_conn job)a do - # variables = %{id: job.id} - # results = user_conn |> query_result(@query, variables, "job") - - # assert results["id"] == to_string(job.id) - # assert is_valid_kv?(results, "favoritedUsers", :list) - # end - - # @query """ - # query Post($id: ID!) { - # post(id: $id) { - # views - # } - # } - # """ - # test "views should +1 after query the post", ~m(user_conn post)a do - # variables = %{id: post.id} - # views_1 = user_conn |> query_result(@query, variables, "post") |> Map.get("views") - - # variables = %{id: post.id} - # views_2 = user_conn |> query_result(@query, variables, "post") |> Map.get("views") - # assert views_2 == views_1 + 1 - # end - - # @query """ - # query Post($id: ID!) { - # post(id: $id) { - # id - # title - # body - # viewerHasFavorited - # } - # } - # """ - # test "logged user can query viewerHasFavorited field", ~m(user_conn post)a do - # variables = %{id: post.id} - - # assert user_conn - # |> query_result(@query, variables, "post") - # |> has_boolen_value?("viewerHasFavorited") - # end - - # test "unlogged user can not query viewerHasFavorited field", ~m(guest_conn post)a do - # variables = %{id: post.id} - - # assert guest_conn |> query_get_error?(@query, variables) - # end - - # @query """ - # query Post($id: ID!) { - # post(id: $id) { - # id - # title - # body - # viewerHasStarred - # } - # } - # """ - # test "logged user can query viewerHasStarred field", ~m(user_conn post)a do - # variables = %{id: post.id} - - # assert user_conn - # |> query_result(@query, variables, "post") - # |> has_boolen_value?("viewerHasStarred") - # end - - # test "unlogged user can not query viewerHasStarred field", ~m(guest_conn post)a do - # variables = %{id: post.id} - # assert guest_conn |> query_get_error?(@query, variables) - # end + @query """ + query($id: ID!) { + job(id: $id) { + id + favoritedUsers { + nickname + id + } + } + } + """ + test "job have favoritedUsers query field", ~m(user_conn job)a do + variables = %{id: job.id} + results = user_conn |> query_result(@query, variables, "job") + + assert results["id"] == to_string(job.id) + assert is_valid_kv?(results, "favoritedUsers", :list) + end + + @query """ + query($id: ID!) { + job(id: $id) { + views + } + } + """ + test "views should +1 after query the job", ~m(user_conn job)a do + variables = %{id: job.id} + views_1 = user_conn |> query_result(@query, variables, "job") |> Map.get("views") + + variables = %{id: job.id} + views_2 = user_conn |> query_result(@query, variables, "job") |> Map.get("views") + assert views_2 == views_1 + 1 + end + + @query """ + query($id: ID!) { + job(id: $id) { + id + title + body + viewerHasFavorited + } + } + """ + test "logged user can query viewerHasFavorited field", ~m(user_conn job)a do + variables = %{id: job.id} + + assert user_conn + |> query_result(@query, variables, "job") + |> has_boolen_value?("viewerHasFavorited") + end + + test "unlogged user can not query viewerHasFavorited field", ~m(guest_conn job)a do + variables = %{id: job.id} + + assert guest_conn |> query_get_error?(@query, variables) + end + + alias MastaniServer.Accounts + + @query """ + query($id: ID!) { + job(id: $id) { + id + favoritedCategoryId + } + } + """ + test "login user can get nil job favorited category id", ~m(job)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + variables = %{id: job.id} + result = user_conn |> query_result(@query, variables, "job") + assert result["favoritedCategoryId"] == nil + end + + test "login user can get job favorited category id after favorited", ~m(job)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, _favorite_category} = Accounts.set_favorites(user, :job, job.id, category.id) + + variables = %{id: job.id} + result = user_conn |> query_result(@query, variables, "job") + + assert result["favoritedCategoryId"] == to_string(category.id) + end end diff --git a/test/mastani_server_web/query/cms/post_test.exs b/test/mastani_server_web/query/cms/post_test.exs index 98ac2279f..39a225f40 100644 --- a/test/mastani_server_web/query/cms/post_test.exs +++ b/test/mastani_server_web/query/cms/post_test.exs @@ -130,7 +130,6 @@ defmodule MastaniServer.Test.Query.Post do } } """ - @tag :wip test "login user can get nil post favorited category id", ~m(post)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -140,7 +139,6 @@ defmodule MastaniServer.Test.Query.Post do assert result["favoritedCategoryId"] == nil end - @tag :wip test "login user can get post favorited category id after favorited", ~m(post)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) diff --git a/test/mastani_server_web/query/cms/video_test.exs b/test/mastani_server_web/query/cms/video_test.exs index 5f27ee776..9e9c57f1c 100644 --- a/test/mastani_server_web/query/cms/video_test.exs +++ b/test/mastani_server_web/query/cms/video_test.exs @@ -52,7 +52,6 @@ defmodule MastaniServer.Test.Query.Video do } } """ - @tag :wip test "login user can get nil video favorited category id", ~m(video)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -62,7 +61,6 @@ defmodule MastaniServer.Test.Query.Video do assert result["favoritedCategoryId"] == nil end - @tag :wip test "login user can get video favorited category id after favorited", ~m(video)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) From ebc85cdfa4534e97af1bfc969882439326788db6 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 14 Oct 2018 21:27:56 +0800 Subject: [PATCH 068/129] fix(user favorite): totalcount mismatch when switch cats --- .../accounts/delegates/favorite_category.ex | 42 ++++++++++++++----- .../schema/utils/common_types.ex | 1 + .../accounts/favorite_category_test.exs | 18 ++++++++ 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/lib/mastani_server/accounts/delegates/favorite_category.ex b/lib/mastani_server/accounts/delegates/favorite_category.ex index 3fb7923e4..8a7f1244b 100644 --- a/lib/mastani_server/accounts/delegates/favorite_category.ex +++ b/lib/mastani_server/accounts/delegates/favorite_category.ex @@ -4,6 +4,8 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do """ import Ecto.Query, warn: false + alias Helper.QueryBuilder + import Helper.ErrorCode import Helper.Utils, only: [done: 1] @@ -13,6 +15,8 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do alias MastaniServer.Accounts.{FavoriteCategory, User} alias MastaniServer.{CMS, Repo} + alias CMS.{PostFavorite, JobFavorite, VideoFavorite} + alias Ecto.Multi def create_favorite_category(%User{id: user_id}, %{title: title} = attrs) do @@ -66,7 +70,7 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do def list_favorite_categories( %User{id: user_id}, %{private: private}, - %{page: page, size: size} + %{page: page, size: size} = filter ) do query = case private do @@ -81,12 +85,11 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do end query + |> QueryBuilder.filter_pack(filter) |> ORM.paginater(page: page, size: size) |> done() end - alias CMS.{PostFavorite, JobFavorite, VideoFavorite} - @doc """ set category for favorited content (post, job, video ...) """ @@ -95,14 +98,27 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do FavoriteCategory |> ORM.find_by(%{user_id: user.id, id: category_id}) do Multi.new() |> Multi.run(:favorite_content, fn _ -> - case find_content_favorite(thread, content_id, user.id) do - {:ok, content_favorite} -> check_dup_category(content_favorite, favorite_category) - {:error, _} -> CMS.reaction(thread, :favorite, content_id, user) + with {:ok, content_favorite} <- find_content_favorite(thread, content_id, user.id) do + check_dup_category(content_favorite, favorite_category) + else + {:error, _} -> + case CMS.reaction(thread, :favorite, content_id, user) do + {:ok, _} -> find_content_favorite(thread, content_id, user.id) + {:error, error} -> {:error, error} + end end end) - |> Multi.run(:update_category_id, fn _ -> - {:ok, content_favorite} = find_content_favorite(thread, content_id, user.id) - + |> Multi.run(:dec_old_category_count, fn %{favorite_content: content_favorite} -> + with false <- is_nil(content_favorite.category_id), + {:ok, old_category} <- FavoriteCategory |> ORM.find(content_favorite.category_id) do + old_category + |> ORM.update(%{total_count: max(old_category.total_count - 1, 0)}) + else + true -> {:ok, ""} + error -> {:error, error} + end + end) + |> Multi.run(:update_content_category_id, fn %{favorite_content: content_favorite} -> content_favorite |> ORM.update(%{category_id: favorite_category.id}) end) |> Multi.run(:update_category_info, fn _ -> @@ -126,7 +142,11 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do {:error, result} end - defp set_favorites_result({:error, :update_category_id, _result, _steps}) do + defp set_favorites_result({:error, :dec_old_category_count, _result, _steps}) do + {:error, [message: "update old category count fails", code: ecode(:update_fails)]} + end + + defp set_favorites_result({:error, :update_content_category_id, _result, _steps}) do {:error, [message: "update category content fails", code: ecode(:update_fails)]} end @@ -180,7 +200,7 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do defp check_dup_category(content, category) do case content.category_id !== category.id do - true -> {:ok, ""} + true -> {:ok, content} false -> {:error, [message: "viewer has already categoried", code: ecode(:already_did)]} end end diff --git a/lib/mastani_server_web/schema/utils/common_types.ex b/lib/mastani_server_web/schema/utils/common_types.ex index 7806c11ff..073a06905 100644 --- a/lib/mastani_server_web/schema/utils/common_types.ex +++ b/lib/mastani_server_web/schema/utils/common_types.ex @@ -34,5 +34,6 @@ defmodule MastaniServerWeb.Schema.Utils.CommonTypes do input_object :common_paged_filter do pagination_args() + field(:sort, :comment_sort_enum, default_value: :desc_inserted) end end diff --git a/test/mastani_server/accounts/favorite_category_test.exs b/test/mastani_server/accounts/favorite_category_test.exs index d04731220..2f7874135 100644 --- a/test/mastani_server/accounts/favorite_category_test.exs +++ b/test/mastani_server/accounts/favorite_category_test.exs @@ -134,6 +134,24 @@ defmodule MastaniServer.Test.Accounts.FavoriteCategory do assert {:error, _} = CMS.PostFavorite |> ORM.find_by(%{post_id: post.id, user_id: user.id}) end + + test "after unset category the old category count should -1", ~m(user post)a do + test_category = "test category" + test_category2 = "test category2" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, category2} = Accounts.create_favorite_category(user, %{title: test_category2}) + + {:ok, _post_favorite} = Accounts.set_favorites(user, :post, post.id, category.id) + {:ok, favorete_cat} = Accounts.FavoriteCategory |> ORM.find(category.id) + assert favorete_cat.total_count == 1 + + {:ok, _post_favorite} = Accounts.set_favorites(user, :post, post.id, category2.id) + {:ok, favorete_cat} = Accounts.FavoriteCategory |> ORM.find(category2.id) + assert favorete_cat.total_count == 1 + + {:ok, favorete_cat} = Accounts.FavoriteCategory |> ORM.find(category.id) + assert favorete_cat.total_count == 0 + end end describe "[favorite category total_count]" do From bbda464c512bb0c62d58de04c2fca32143d22606 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 15 Oct 2018 15:04:52 +0800 Subject: [PATCH 069/129] fix(user reputation): fix reputation calc by refactor missing feature: downgrade author's reputation when replated user deleted their favroted category --- lib/helper/utils.ex | 11 +++ lib/mastani_server/accounts/accounts.ex | 1 + .../accounts/delegates/achievements.ex | 32 ++++++--- .../accounts/delegates/favorite_category.ex | 70 ++++++++++++++----- .../accounts/achievement_test.exs | 12 ++-- .../accounts/favorite_category_test.exs | 52 +++++++++++++- .../query/accounts/achievement_test.exs | 7 +- 7 files changed, 147 insertions(+), 38 deletions(-) diff --git a/lib/helper/utils.ex b/lib/helper/utils.ex index e0bcb7585..efa9649e5 100644 --- a/lib/helper/utils.ex +++ b/lib/helper/utils.ex @@ -140,4 +140,15 @@ defmodule Helper.Utils do # NOT a map. We fall back to standard merge behavior, preferring # the value on the right. defp deep_resolve(_key, _left, right), do: right + + @doc """ + ["a", "b", "c", "c"] => %{"a" => 1, "b" => 1, "c" => 2} + """ + def count_words(words) when is_list(words) do + Enum.reduce(words, %{}, &update_word_count/2) + end + + defp update_word_count(word, acc) do + Map.update(acc, to_string(word), 1, &(&1 + 1)) + end end diff --git a/lib/mastani_server/accounts/accounts.ex b/lib/mastani_server/accounts/accounts.ex index ce0488349..59bd4ff51 100644 --- a/lib/mastani_server/accounts/accounts.ex +++ b/lib/mastani_server/accounts/accounts.ex @@ -31,6 +31,7 @@ defmodule MastaniServer.Accounts do # achievement defdelegate achieve(user, operation, key), to: Achievements defdelegate list_editable_communities(user, filter), to: Achievements + defdelegate downgrade_achievement(user, action, count), to: Achievements # defdelegate list_editable_communities(filter), to: Achievements # publish diff --git a/lib/mastani_server/accounts/delegates/achievements.ex b/lib/mastani_server/accounts/delegates/achievements.ex index d37fa0318..6bf21fad1 100644 --- a/lib/mastani_server/accounts/delegates/achievements.ex +++ b/lib/mastani_server/accounts/delegates/achievements.ex @@ -25,7 +25,7 @@ defmodule MastaniServer.Accounts.Delegate.Achievements do @spec achieve(User.t(), atom, atom) :: SpecType.done() def achieve(%User{id: user_id}, :add, :follow) do with {:ok, achievement} <- ORM.findby_or_insert(Achievement, ~m(user_id)a, ~m(user_id)a) do - followers_count = achievement.followers_count + @follow_weight + followers_count = achievement.followers_count + 1 reputation = achievement.reputation + @follow_weight achievement @@ -38,8 +38,8 @@ defmodule MastaniServer.Accounts.Delegate.Achievements do """ def achieve(%User{id: user_id}, :minus, :follow) do with {:ok, achievement} <- ORM.findby_or_insert(Achievement, ~m(user_id)a, ~m(user_id)a) do - followers_count = achievement.followers_count |> safe_minus(@follow_weight) - reputation = achievement.reputation |> safe_minus(@follow_weight) + followers_count = max(achievement.followers_count - 1, 0) + reputation = max(achievement.reputation - @follow_weight, 0) achievement |> ORM.update(~m(followers_count reputation)a) @@ -51,7 +51,7 @@ defmodule MastaniServer.Accounts.Delegate.Achievements do """ def achieve(%User{id: user_id} = _user, :add, :star) do with {:ok, achievement} <- ORM.findby_or_insert(Achievement, ~m(user_id)a, ~m(user_id)a) do - contents_stared_count = achievement.contents_stared_count + @star_weight + contents_stared_count = achievement.contents_stared_count + 1 reputation = achievement.reputation + @star_weight achievement @@ -64,8 +64,8 @@ defmodule MastaniServer.Accounts.Delegate.Achievements do """ def achieve(%User{id: user_id} = _user, :minus, :star) do with {:ok, achievement} <- ORM.findby_or_insert(Achievement, ~m(user_id)a, ~m(user_id)a) do - contents_stared_count = achievement.contents_stared_count |> safe_minus(@star_weight) - reputation = achievement.reputation |> safe_minus(@star_weight) + contents_stared_count = max(achievement.contents_stared_count - 1, 0) + reputation = max(achievement.reputation - @star_weight, 0) achievement |> ORM.update(~m(contents_stared_count reputation)a) @@ -77,7 +77,7 @@ defmodule MastaniServer.Accounts.Delegate.Achievements do """ def achieve(%User{id: user_id} = _user, :add, :favorite) do with {:ok, achievement} <- ORM.findby_or_insert(Achievement, ~m(user_id)a, ~m(user_id)a) do - contents_favorited_count = achievement.contents_favorited_count + @favorite_weight + contents_favorited_count = achievement.contents_favorited_count + 1 reputation = achievement.reputation + @favorite_weight achievement @@ -90,10 +90,9 @@ defmodule MastaniServer.Accounts.Delegate.Achievements do """ def achieve(%User{id: user_id} = _user, :minus, :favorite) do with {:ok, achievement} <- ORM.findby_or_insert(Achievement, ~m(user_id)a, ~m(user_id)a) do - contents_favorited_count = - achievement.contents_favorited_count |> safe_minus(@favorite_weight) + contents_favorited_count = max(achievement.contents_favorited_count - 1, 0) - reputation = achievement.reputation |> safe_minus(@favorite_weight) + reputation = max(achievement.reputation - @favorite_weight, 0) achievement |> ORM.update(~m(contents_favorited_count reputation)a) @@ -112,6 +111,19 @@ defmodule MastaniServer.Accounts.Delegate.Achievements do # IO.inspect("acheiveements plus") # end + @doc """ + only used for user delete the farorited category, other case is auto + """ + def downgrade_achievement(%User{id: user_id}, :favorite, count) do + with {:ok, achievement} <- ORM.find_by(Achievement, user_id: user_id) do + contents_favorited_count = max(achievement.contents_favorited_count - count, 0) + reputation = max(achievement.reputation - count * @favorite_weight, 0) + + achievement + |> ORM.update(~m(contents_favorited_count reputation)a) + end + end + @spec safe_minus(non_neg_integer(), non_neg_integer()) :: non_neg_integer() defp safe_minus(count, unit) when is_integer(count) and is_integer(unit) and unit > 0 do case count <= 0 do diff --git a/lib/mastani_server/accounts/delegates/favorite_category.ex b/lib/mastani_server/accounts/delegates/favorite_category.ex index 8a7f1244b..dcaaf9ade 100644 --- a/lib/mastani_server/accounts/delegates/favorite_category.ex +++ b/lib/mastani_server/accounts/delegates/favorite_category.ex @@ -7,11 +7,12 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do alias Helper.QueryBuilder import Helper.ErrorCode - import Helper.Utils, only: [done: 1] + import Helper.Utils, only: [done: 1, count_words: 1] import ShortMaps alias Helper.ORM + alias MastaniServer.Accounts alias MastaniServer.Accounts.{FavoriteCategory, User} alias MastaniServer.{CMS, Repo} @@ -39,25 +40,62 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do def delete_favorite_category(%User{id: user_id}, id) do with {:ok, category} <- FavoriteCategory |> ORM.find_by(~m(id user_id)a) do Multi.new() + |> Multi.run(:downgrade_achievement, fn _ -> + # find user favvoried-contents(posts & jobs & videos) 's author, + # and downgrade their's acieveents + # NOTE: this is too fucking violent and should be refactor later + # we find favroted posts/jobs/videos author_ids then doengrade their achievement + # this implentment is limited, if the user have lots contents in a favoreted-category + # ant those contents have diffenert author each, it may be fucked + # should be in a queue job or sth + {:ok, post_author_ids} = affected_author_ids(:post, CMS.PostFavorite, category) + {:ok, job_author_ids} = affected_author_ids(:job, CMS.JobFavorite, category) + {:ok, video_author_ids} = affected_author_ids(:video, CMS.VideoFavorite, category) + + # author_ids_list = count_words(total_author_ids) |> Map.to_list + author_ids_list = + (post_author_ids ++ job_author_ids ++ video_author_ids) |> count_words |> Map.to_list() + + # NOTE: if the contents have too many unique authors, it may be crash the server + # so limit size to 20 unique authors + Enum.each(author_ids_list |> Enum.slice(0, 20), fn {author_id, count} -> + Accounts.downgrade_achievement(%User{id: author_id}, :favorite, count) + end) + + {:ok, %{done: true}} + end) |> Multi.run(:delete_category, fn _ -> category |> ORM.delete() end) - |> Multi.run(:delete_favorite_record, fn _ -> - query = - from( - pf in CMS.PostFavorite, - where: pf.user_id == ^user_id, - where: pf.category_id == ^category.id - ) - - query |> Repo.delete_all() |> done() - end) |> Repo.transaction() |> delete_favorites_result() end end - defp delete_favorites_result({:ok, %{delete_favorite_record: result}}), do: {:ok, result} + # NOTE: this is too fucking violent and should be refactor later + # we find favroted posts/jobs/videos author_ids then doengrade their achievement + # this implentment is limited, if the user have lots contents in a favoreted-category + # ant those contents have diffenert author each, it may be fucked + defp affected_author_ids(thread, queryable, category) do + query = + from( + fc in queryable, + join: content in assoc(fc, ^thread), + join: author in assoc(content, :author), + where: fc.category_id == ^category.id, + select: author.user_id + ) + + case ORM.find_all(query, %{page: 1, size: 50}) do + {:ok, paged_contents} -> + {:ok, paged_contents |> Map.get(:entries)} + + {:error, _} -> + {:ok, []} + end + end + + defp delete_favorites_result({:ok, %{downgrade_achievement: result}}), do: {:ok, result} defp delete_favorites_result({:error, :delete_category, _result, _steps}) do {:error, [message: "delete category fails", code: ecode(:delete_fails)]} @@ -158,10 +196,8 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do with {:ok, favorite_category} <- FavoriteCategory |> ORM.find_by(%{user_id: user.id, id: category_id}) do Multi.new() - |> Multi.run(:remove_favorite_record, fn _ -> - {:ok, content_favorite} = find_content_favorite(thread, content_id, user.id) - - content_favorite |> ORM.delete() + |> Multi.run(:undo_favorite_action, fn _ -> + CMS.undo_reaction(thread, :favorite, content_id, user) end) |> Multi.run(:update_category_info, fn _ -> last_updated = Timex.today() |> Timex.to_datetime() @@ -180,7 +216,7 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do # @spec unset_favorites_result({:ok, map()}) :: {:ok, FavoriteCategory.t() } defp unset_favorites_result({:ok, %{update_category_info: result}}), do: {:ok, result} - defp unset_favorites_result({:error, :remove_favorite_record, result, _steps}) do + defp unset_favorites_result({:error, :undo_favorite_action, result, _steps}) do # {:error, [message: "favorite content fails", code: ecode(:react_fails)]} {:error, result} end diff --git a/test/mastani_server/accounts/achievement_test.exs b/test/mastani_server/accounts/achievement_test.exs index 214b54783..a2a487368 100644 --- a/test/mastani_server/accounts/achievement_test.exs +++ b/test/mastani_server/accounts/achievement_test.exs @@ -57,9 +57,9 @@ defmodule MastaniServer.Test.Accounts.Achievement do user |> Accounts.achieve(:add, :favorite) {:ok, achievement} = Achievement |> ORM.find_by(user_id: user.id) - assert achievement.followers_count == @follow_weight - assert achievement.contents_stared_count == @star_weight - assert achievement.contents_favorited_count == @favorite_weight + assert achievement.followers_count == 1 + assert achievement.contents_stared_count == 1 + assert achievement.contents_favorited_count == 1 reputation = @follow_weight + @star_weight + @favorite_weight assert achievement.reputation == reputation @@ -99,7 +99,7 @@ defmodule MastaniServer.Test.Accounts.Achievement do {:ok, user} = User |> ORM.find(user.id, preload: :achievement) - assert user.achievement.followers_count == @follow_weight * total_count + assert user.achievement.followers_count == total_count assert user.achievement.reputation == @follow_weight * total_count end @@ -116,8 +116,8 @@ defmodule MastaniServer.Test.Accounts.Achievement do {:ok, user} = User |> ORM.find(user.id, preload: :achievement) - assert user.achievement.followers_count == @follow_weight * (total_count - 1) - assert user.achievement.reputation == @follow_weight * (total_count - 1) + assert user.achievement.followers_count == total_count - 1 + assert user.achievement.reputation == @follow_weight * total_count - @follow_weight end end end diff --git a/test/mastani_server_web/mutation/accounts/favorite_category_test.exs b/test/mastani_server_web/mutation/accounts/favorite_category_test.exs index ebbe3da41..4de9c3cbe 100644 --- a/test/mastani_server_web/mutation/accounts/favorite_category_test.exs +++ b/test/mastani_server_web/mutation/accounts/favorite_category_test.exs @@ -9,11 +9,13 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do setup do {:ok, user} = db_insert(:user) {:ok, post} = db_insert(:post) + {:ok, job} = db_insert(:job) + {:ok, video} = db_insert(:video) user_conn = simu_conn(:user, user) guest_conn = simu_conn(:guest) - {:ok, ~m(user_conn guest_conn user post)a} + {:ok, ~m(user_conn guest_conn user post job video)a} end describe "[Accounts FavoriteCategory CURD]" do @@ -95,6 +97,54 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do assert deleted["done"] == true assert {:error, _} = FavoriteCategory |> ORM.find(category.id) end + + test "after favorite deleted, the favroted action also be deleted ", + ~m(user_conn user post)a do + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + + {:ok, _favorite_category} = Accounts.set_favorites(user, :post, post.id, category.id) + + assert {:ok, _} = + CMS.PostFavorite |> ORM.find_by(%{user_id: user.id, category_id: category.id}) + + variables = %{id: category.id} + user_conn |> mutation_result(@query, variables, "deleteFavoriteCategory") + + assert {:error, _} = + CMS.PostFavorite |> ORM.find_by(%{user_id: user.id, category_id: category.id}) + end + + test "after favorite deleted, the related author's reputation should be downgrade", + ~m(user_conn user post job)a do + {:ok, author} = db_insert(:author) + {:ok, post2} = db_insert(:post, %{author: author}) + {:ok, job2} = db_insert(:job, %{author: author}) + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + + test_category2 = "test category2" + {:ok, category2} = Accounts.create_favorite_category(user, %{title: test_category2}) + + {:ok, _} = Accounts.set_favorites(user, :post, post.id, category.id) + {:ok, _} = Accounts.set_favorites(user, :post, post2.id, category.id) + {:ok, _} = Accounts.set_favorites(user, :job, job.id, category.id) + {:ok, _} = Accounts.set_favorites(user, :job, job2.id, category2.id) + + # author.id + {:ok, achievement} = ORM.find_by(Accounts.Achievement, user_id: author.user.id) + + assert achievement.contents_favorited_count == 2 + assert achievement.reputation == 4 + + variables = %{id: category.id} + user_conn |> mutation_result(@query, variables, "deleteFavoriteCategory") + + {:ok, achievement} = ORM.find_by(Accounts.Achievement, user_id: author.user.id) + + assert achievement.contents_favorited_count == 1 + assert achievement.reputation == 2 + end end describe "[Accounts FavoriteCategory set/unset]" do diff --git a/test/mastani_server_web/query/accounts/achievement_test.exs b/test/mastani_server_web/query/accounts/achievement_test.exs index add960852..7b2a08ada 100644 --- a/test/mastani_server_web/query/accounts/achievement_test.exs +++ b/test/mastani_server_web/query/accounts/achievement_test.exs @@ -167,7 +167,7 @@ defmodule MastaniServer.Test.Query.Account.Achievement do variables = %{id: author_user_id} results = guest_conn |> query_result(@query, variables, "user") - assert results["achievement"] |> Map.get("contentsFavoritedCount") == @favorite_weight + assert results["achievement"] |> Map.get("contentsFavoritedCount") == 1 assert results["achievement"] |> Map.get("reputation") == @favorite_weight end @@ -189,11 +189,10 @@ defmodule MastaniServer.Test.Query.Account.Achievement do variables = %{id: author_user_id} results = guest_conn |> query_result(@query, variables, "user") - assert results["achievement"] |> Map.get("contentsFavoritedCount") == - @favorite_weight * (total_count - 1) + assert results["achievement"] |> Map.get("contentsFavoritedCount") == total_count - 1 assert results["achievement"] |> Map.get("reputation") == - @favorite_weight * (total_count - 1) + @favorite_weight * total_count - @favorite_weight end end end From 1451208c2423d8a21c30da7def16e314ca0e1344 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 15 Oct 2018 15:11:50 +0800 Subject: [PATCH 070/129] refactor(contents reaction): block favorite action from react contents favroite content should set category first, and must have a belonged category --- lib/mastani_server_web/schema/cms/cms_misc.ex | 5 +++++ lib/mastani_server_web/schema/cms/mutations/operation.ex | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index 3b044b5b6..2264fd19f 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -33,6 +33,11 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do # value(:watch) end + enum :reactable_action do + value(:star) + # value(:watch) + end + enum :react_thread do value(:post) value(:job) diff --git a/lib/mastani_server_web/schema/cms/mutations/operation.ex b/lib/mastani_server_web/schema/cms/mutations/operation.ex index aa3f61ea7..86ced2a1a 100644 --- a/lib/mastani_server_web/schema/cms/mutations/operation.ex +++ b/lib/mastani_server_web/schema/cms/mutations/operation.ex @@ -129,11 +129,11 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Operation do resolve(&R.CMS.unset_community/3) end - @desc "react on a cms content" + @desc "react on a cms content, except favorite" field :reaction, :article do arg(:id, non_null(:id)) arg(:thread, non_null(:react_thread)) - arg(:action, non_null(:react_action)) + arg(:action, non_null(:reactable_action)) middleware(M.Authorize, :login) resolve(&R.CMS.reaction/3) @@ -143,7 +143,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Operation do field :undo_reaction, :article do arg(:id, non_null(:id)) arg(:thread, non_null(:react_thread)) - arg(:action, non_null(:react_action)) + arg(:action, non_null(:reactable_action)) middleware(M.Authorize, :login) resolve(&R.CMS.undo_reaction/3) From c8857671ddac9e737be8a485703a8dfaf4a11f58 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 16 Oct 2018 14:47:29 +0800 Subject: [PATCH 071/129] test(favrotie reaction tests): removed, use favroted category instead --- .../mutation/cms/job_reaction_test.exs | 46 ------------------- .../mutation/cms/post_reaction_test.exs | 46 ------------------- .../mutation/cms/video_reaction_test.exs | 46 ------------------- 3 files changed, 138 deletions(-) diff --git a/test/mastani_server_web/mutation/cms/job_reaction_test.exs b/test/mastani_server_web/mutation/cms/job_reaction_test.exs index 6c5d2c373..927d08d17 100644 --- a/test/mastani_server_web/mutation/cms/job_reaction_test.exs +++ b/test/mastani_server_web/mutation/cms/job_reaction_test.exs @@ -13,52 +13,6 @@ defmodule MastaniServer.Test.Mutation.JobReaction do {:ok, ~m(user_conn guest_conn job user)a} end - describe "[job favorite]" do - @query """ - mutation($id: ID!, $action: String!, $thread: CmsThread!) { - reaction(id: $id, action: $action, thread: $thread) { - id - } - } - """ - test "login user can favorite a job", ~m(user_conn job)a do - variables = %{id: job.id, thread: "JOB", action: "FAVORITE"} - created = user_conn |> mutation_result(@query, variables, "reaction") - - assert created["id"] == to_string(job.id) - end - - test "unauth user favorite a job fails", ~m(guest_conn job)a do - variables = %{id: job.id, thread: "JOB", action: "FAVORITE"} - - assert guest_conn - |> mutation_get_error?(@query, variables, ecode(:account_login)) - end - - @query """ - mutation($id: ID!, $action: String!, $thread: CmsThread!) { - undoReaction(id: $id, action: $action, thread: $thread) { - id - } - } - """ - test "login user can undo favorite a job", ~m(user_conn job user)a do - {:ok, _} = CMS.reaction(:job, :favorite, job.id, user) - - variables = %{id: job.id, thread: "JOB", action: "FAVORITE"} - updated = user_conn |> mutation_result(@query, variables, "undoReaction") - - assert updated["id"] == to_string(job.id) - end - - test "unauth user undo favorite a job fails", ~m(guest_conn job)a do - variables = %{id: job.id, thread: "JOB", action: "FAVORITE"} - - assert guest_conn - |> mutation_get_error?(@query, variables, ecode(:account_login)) - end - end - describe "[job star]" do @query """ mutation($id: ID!, $action: String!, $thread: CmsThread!) { diff --git a/test/mastani_server_web/mutation/cms/post_reaction_test.exs b/test/mastani_server_web/mutation/cms/post_reaction_test.exs index b82cdcee4..296252d2e 100644 --- a/test/mastani_server_web/mutation/cms/post_reaction_test.exs +++ b/test/mastani_server_web/mutation/cms/post_reaction_test.exs @@ -13,52 +13,6 @@ defmodule MastaniServer.Test.Mutation.PostReaction do {:ok, ~m(user_conn guest_conn post user)a} end - describe "[post favorite]" do - @query """ - mutation($id: ID!, $action: String!, $thread: CmsThread!) { - reaction(id: $id, action: $action, thread: $thread) { - id - } - } - """ - test "login user can favorite a post", ~m(user_conn post)a do - variables = %{id: post.id, thread: "POST", action: "FAVORITE"} - created = user_conn |> mutation_result(@query, variables, "reaction") - - assert created["id"] == to_string(post.id) - end - - test "unauth user favorite a post fails", ~m(guest_conn post)a do - variables = %{id: post.id, thread: "POST", action: "FAVORITE"} - - assert guest_conn - |> mutation_get_error?(@query, variables, ecode(:account_login)) - end - - @query """ - mutation($id: ID!, $action: String!, $thread: CmsThread!) { - undoReaction(id: $id, action: $action, thread: $thread) { - id - } - } - """ - test "login user can undo favorite a post", ~m(user_conn post user)a do - {:ok, _} = CMS.reaction(:post, :favorite, post.id, user) - - variables = %{id: post.id, thread: "POST", action: "FAVORITE"} - updated = user_conn |> mutation_result(@query, variables, "undoReaction") - - assert updated["id"] == to_string(post.id) - end - - test "unauth user undo favorite a post fails", ~m(guest_conn post)a do - variables = %{id: post.id, thread: "POST", action: "FAVORITE"} - - assert guest_conn - |> mutation_get_error?(@query, variables, ecode(:account_login)) - end - end - describe "[post star]" do @query """ mutation($id: ID!, $action: String!, $thread: CmsThread!) { diff --git a/test/mastani_server_web/mutation/cms/video_reaction_test.exs b/test/mastani_server_web/mutation/cms/video_reaction_test.exs index fe20816c4..80787461b 100644 --- a/test/mastani_server_web/mutation/cms/video_reaction_test.exs +++ b/test/mastani_server_web/mutation/cms/video_reaction_test.exs @@ -13,52 +13,6 @@ defmodule MastaniServer.Test.Mutation.VideoReaction do {:ok, ~m(user_conn guest_conn video user)a} end - describe "[video favorite]" do - @query """ - mutation($id: ID!, $action: String!, $thread: CmsThread!) { - reaction(id: $id, action: $action, thread: $thread) { - id - } - } - """ - test "login user can favorite a video", ~m(user_conn video)a do - variables = %{id: video.id, thread: "VIDEO", action: "FAVORITE"} - created = user_conn |> mutation_result(@query, variables, "reaction") - - assert created["id"] == to_string(video.id) - end - - test "unauth user favorite a video fails", ~m(guest_conn video)a do - variables = %{id: video.id, thread: "VIDEO", action: "FAVORITE"} - - assert guest_conn - |> mutation_get_error?(@query, variables, ecode(:account_login)) - end - - @query """ - mutation($id: ID!, $action: String!, $thread: CmsThread!) { - undoReaction(id: $id, action: $action, thread: $thread) { - id - } - } - """ - test "login user can undo favorite a video", ~m(user_conn video user)a do - {:ok, _} = CMS.reaction(:video, :favorite, video.id, user) - - variables = %{id: video.id, thread: "VIDEO", action: "FAVORITE"} - updated = user_conn |> mutation_result(@query, variables, "undoReaction") - - assert updated["id"] == to_string(video.id) - end - - test "unauth user undo favorite a video fails", ~m(guest_conn video)a do - variables = %{id: video.id, thread: "VIDEO", action: "FAVORITE"} - - assert guest_conn - |> mutation_get_error?(@query, variables, ecode(:account_login)) - end - end - describe "[video star]" do @query """ mutation($id: ID!, $action: String!, $thread: CmsThread!) { From 4e27a43aec5d613d00f7cb50e8865871c81ad1a3 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 16 Oct 2018 15:30:52 +0800 Subject: [PATCH 072/129] feat(user published): publisehd comments --- lib/mastani_server/accounts/accounts.ex | 1 + .../accounts/delegates/publish.ex | 22 ++- .../resolvers/accounts_resolver.ex | 9 + .../schema/account/account_queries.ex | 10 + lib/mastani_server_web/schema/cms/cms_misc.ex | 7 + .../accounts/published_comments_test.exs | 146 ++++++++++++++ .../accounts/published_comments_test.exs | 181 ++++++++++++++++++ .../accounts/published_contents_test.exs | 8 +- 8 files changed, 377 insertions(+), 7 deletions(-) create mode 100644 test/mastani_server/accounts/published_comments_test.exs create mode 100644 test/mastani_server_web/query/accounts/published_comments_test.exs diff --git a/lib/mastani_server/accounts/accounts.ex b/lib/mastani_server/accounts/accounts.ex index 59bd4ff51..944b52613 100644 --- a/lib/mastani_server/accounts/accounts.ex +++ b/lib/mastani_server/accounts/accounts.ex @@ -36,6 +36,7 @@ defmodule MastaniServer.Accounts do # publish defdelegate published_contents(user, thread, filter), to: Publish + defdelegate published_comments(user, thread, filter), to: Publish # fans defdelegate follow(user, follower), to: Fans diff --git a/lib/mastani_server/accounts/delegates/publish.ex b/lib/mastani_server/accounts/delegates/publish.ex index 32641d87b..d395a4e3c 100644 --- a/lib/mastani_server/accounts/delegates/publish.ex +++ b/lib/mastani_server/accounts/delegates/publish.ex @@ -22,9 +22,25 @@ defmodule MastaniServer.Accounts.Delegate.Publish do with {:ok, user} <- ORM.find(User, user_id), {:ok, content} <- match_action(thread, :self) do content.target - |> join(:inner, [p], a in assoc(p, :author)) - |> where([p, a], a.user_id == ^user.id) - |> select([p, a], p) + |> join(:inner, [content], author in assoc(content, :author)) + |> where([content, author], author.user_id == ^user.id) + |> select([content, author], content) + |> QueryBuilder.filter_pack(filter) + |> ORM.paginater(~m(page size)a) + |> done() + end + end + + @doc """ + get paged published comments of a user + """ + def published_comments(%User{id: user_id}, thread, %{page: page, size: size} = filter) do + with {:ok, user} <- ORM.find(User, user_id), + {:ok, content} <- match_action(thread, :comment) do + content.reactor + |> join(:inner, [comment], author in assoc(comment, :author)) + |> where([comment, author], author.id == ^user.id) + |> select([comment, author], comment) |> QueryBuilder.filter_pack(filter) |> ORM.paginater(~m(page size)a) |> done() diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 08f9a45ef..65e583332 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -125,6 +125,15 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.published_contents(cur_user, thread, filter) end + # published comments + def published_comments(_root, ~m(user_id filter thread)a, _info) do + Accounts.published_comments(%User{id: user_id}, thread, filter) + end + + def published_comments(_root, ~m(filter thread)a, %{context: %{cur_user: cur_user}}) do + Accounts.published_comments(cur_user, thread, filter) + end + # paged communities which the user it's the editor def editable_communities(_root, ~m(user_id filter)a, _info) do Accounts.list_editable_communities(%User{id: user_id}, filter) diff --git a/lib/mastani_server_web/schema/account/account_queries.ex b/lib/mastani_server_web/schema/account/account_queries.ex index 67c540142..b1b8b31d0 100644 --- a/lib/mastani_server_web/schema/account/account_queries.ex +++ b/lib/mastani_server_web/schema/account/account_queries.ex @@ -171,6 +171,16 @@ defmodule MastaniServerWeb.Schema.Account.Queries do resolve(&R.Accounts.published_contents/3) end + @desc "get paged published comments on post" + field :published_comments, :paged_comments do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:thread, :commentable_thread, default_value: :post) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.published_comments/3) + end + @desc "paged communities which the user it's the editor" field :editable_communities, :paged_communities do arg(:user_id, :id) diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index 2264fd19f..0d0f55ebd 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -51,6 +51,13 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do value(:repo_comment) end + enum :commentable_thread do + value(:post) + value(:job) + value(:video) + value(:repo) + end + enum :cms_thread do value(:post) value(:job) diff --git a/test/mastani_server/accounts/published_comments_test.exs b/test/mastani_server/accounts/published_comments_test.exs new file mode 100644 index 000000000..3d1c164a1 --- /dev/null +++ b/test/mastani_server/accounts/published_comments_test.exs @@ -0,0 +1,146 @@ +defmodule MastaniServer.Test.Accounts.PublishedComments do + use MastaniServer.TestTools + + alias MastaniServer.{Accounts, CMS} + + @publish_count 10 + + setup do + {:ok, user} = db_insert(:user) + {:ok, user2} = db_insert(:user) + + {:ok, ~m(user user2)a} + end + + describe "[Accounts Publised post comments]" do + test "fresh user get empty paged published posts", ~m(user)a do + {:ok, results} = Accounts.published_comments(user, :post, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == 0 + end + + test "user can get paged published posts", ~m(user user2)a do + body = "this is a test comment" + {:ok, post} = db_insert(:post) + {:ok, post2} = db_insert(:post) + + pub_comments = + Enum.reduce(1..@publish_count, [], fn _, acc -> + body = "this is a test comment" + {:ok, comment} = CMS.create_comment(:post, post.id, body, user) + acc ++ [comment] + end) + + {:ok, _comment} = CMS.create_comment(:post, post2.id, body, user) + {:ok, _comment} = CMS.create_comment(:post, post2.id, body, user2) + + {:ok, results} = Accounts.published_comments(user, :post, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == @publish_count + 1 + + random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) + assert results.entries |> Enum.any?(&(&1.id == random_comment_id)) + end + end + + describe "[Accounts Publised job comments]" do + test "fresh user get empty paged published jobs", ~m(user)a do + {:ok, results} = Accounts.published_comments(user, :job, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == 0 + end + + test "user can get paged published jobs", ~m(user user2)a do + body = "this is a test comment" + {:ok, job} = db_insert(:job) + {:ok, job2} = db_insert(:job) + + pub_comments = + Enum.reduce(1..@publish_count, [], fn _, acc -> + body = "this is a test comment" + {:ok, comment} = CMS.create_comment(:job, job.id, body, user) + acc ++ [comment] + end) + + {:ok, _comment} = CMS.create_comment(:job, job2.id, body, user) + {:ok, _comment} = CMS.create_comment(:job, job2.id, body, user2) + + {:ok, results} = Accounts.published_comments(user, :job, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == @publish_count + 1 + + random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) + assert results.entries |> Enum.any?(&(&1.id == random_comment_id)) + end + end + + describe "[Accounts Publised video comments]" do + test "fresh user get empty paged published videos", ~m(user)a do + {:ok, results} = Accounts.published_comments(user, :video, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == 0 + end + + test "user can get paged published videos", ~m(user user2)a do + body = "this is a test comment" + {:ok, video} = db_insert(:video) + {:ok, video2} = db_insert(:video) + + pub_comments = + Enum.reduce(1..@publish_count, [], fn _, acc -> + body = "this is a test comment" + {:ok, comment} = CMS.create_comment(:video, video.id, body, user) + acc ++ [comment] + end) + + {:ok, _comment} = CMS.create_comment(:video, video2.id, body, user) + {:ok, _comment} = CMS.create_comment(:video, video2.id, body, user2) + + {:ok, results} = Accounts.published_comments(user, :video, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == @publish_count + 1 + + random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) + assert results.entries |> Enum.any?(&(&1.id == random_comment_id)) + end + end + + describe "[Accounts Publised repo comments]" do + test "fresh user get empty paged published repos", ~m(user)a do + {:ok, results} = Accounts.published_comments(user, :repo, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == 0 + end + + test "user can get paged published repos", ~m(user user2)a do + body = "this is a test comment" + {:ok, repo} = db_insert(:repo) + {:ok, repo2} = db_insert(:repo) + + pub_comments = + Enum.reduce(1..@publish_count, [], fn _, acc -> + body = "this is a test comment" + {:ok, comment} = CMS.create_comment(:repo, repo.id, body, user) + acc ++ [comment] + end) + + {:ok, _comment} = CMS.create_comment(:repo, repo2.id, body, user) + {:ok, _comment} = CMS.create_comment(:repo, repo2.id, body, user2) + + {:ok, results} = Accounts.published_comments(user, :repo, %{page: 1, size: 20}) + + assert results |> is_valid_pagination?(:raw) + assert results.total_count == @publish_count + 1 + + random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) + assert results.entries |> Enum.any?(&(&1.id == random_comment_id)) + end + end +end diff --git a/test/mastani_server_web/query/accounts/published_comments_test.exs b/test/mastani_server_web/query/accounts/published_comments_test.exs new file mode 100644 index 000000000..f495efbfb --- /dev/null +++ b/test/mastani_server_web/query/accounts/published_comments_test.exs @@ -0,0 +1,181 @@ +defmodule MastaniServer.Test.Query.Accounts.PublishedComments do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + @publish_count 10 + + setup do + {:ok, user} = db_insert(:user) + {:ok, community} = db_insert(:community) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(guest_conn user_conn user community)a} + end + + describe "[account published comments on post]" do + @query """ + query($userId: ID!, $thread: CommentableThread, $filter: PagedFilter!) { + publishedComments(userId: $userId, thread: $thread, filter: $filter) { + entries { + id + body + author { + id + } + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + test "user can get paged published comments on post", ~m(guest_conn user community)a do + {:ok, post} = db_insert(:post) + + pub_comments = + Enum.reduce(1..@publish_count, [], fn _, acc -> + body = "this is a test comment" + {:ok, comment} = CMS.create_comment(:post, post.id, body, user) + acc ++ [comment] + end) + + random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) |> to_string + + variables = %{userId: user.id, thread: "POST", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedComments") + + assert results |> is_valid_pagination? + assert results["totalCount"] == @publish_count + + assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == random_comment_id)) + end + end + + describe "[account published comments on job]" do + @query """ + query($userId: ID!, $thread: CommentableThread, $filter: PagedFilter!) { + publishedComments(userId: $userId, thread: $thread, filter: $filter) { + entries { + id + body + author { + id + } + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + test "user can get paged published comments on job", ~m(guest_conn user community)a do + {:ok, job} = db_insert(:job) + + pub_comments = + Enum.reduce(1..@publish_count, [], fn _, acc -> + body = "this is a test comment" + {:ok, comment} = CMS.create_comment(:job, job.id, body, user) + acc ++ [comment] + end) + + random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) |> to_string + + variables = %{userId: user.id, thread: "JOB", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedComments") + + assert results |> is_valid_pagination? + assert results["totalCount"] == @publish_count + + assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == random_comment_id)) + end + end + + describe "[account published comments on video]" do + @query """ + query($userId: ID!, $thread: CommentableThread, $filter: PagedFilter!) { + publishedComments(userId: $userId, thread: $thread, filter: $filter) { + entries { + id + body + author { + id + } + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + test "user can get paged published comments on video", ~m(guest_conn user community)a do + {:ok, video} = db_insert(:video) + + pub_comments = + Enum.reduce(1..@publish_count, [], fn _, acc -> + body = "this is a test comment" + {:ok, comment} = CMS.create_comment(:video, video.id, body, user) + acc ++ [comment] + end) + + random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) |> to_string + + variables = %{userId: user.id, thread: "VIDEO", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedComments") + + assert results |> is_valid_pagination? + assert results["totalCount"] == @publish_count + + assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == random_comment_id)) + end + end + + describe "[account published comments on repo]" do + @query """ + query($userId: ID!, $thread: CommentableThread, $filter: PagedFilter!) { + publishedComments(userId: $userId, thread: $thread, filter: $filter) { + entries { + id + body + author { + id + } + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + test "user can get paged published comments on repo", ~m(guest_conn user community)a do + {:ok, repo} = db_insert(:repo) + + pub_comments = + Enum.reduce(1..@publish_count, [], fn _, acc -> + body = "this is a test comment" + {:ok, comment} = CMS.create_comment(:repo, repo.id, body, user) + acc ++ [comment] + end) + + random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) |> to_string + + variables = %{userId: user.id, thread: "REPO", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedComments") + + assert results |> is_valid_pagination? + assert results["totalCount"] == @publish_count + + assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == random_comment_id)) + end + end +end diff --git a/test/mastani_server_web/query/accounts/published_contents_test.exs b/test/mastani_server_web/query/accounts/published_contents_test.exs index ed70533f6..7057a7d1d 100644 --- a/test/mastani_server_web/query/accounts/published_contents_test.exs +++ b/test/mastani_server_web/query/accounts/published_contents_test.exs @@ -15,7 +15,7 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedContents do {:ok, ~m(guest_conn user_conn user community)a} end - describe "[account favorited posts]" do + describe "[account published posts]" do @query """ query($userId: ID!, $filter: PagedFilter!) { publishedPosts(userId: $userId, filter: $filter) { @@ -54,7 +54,7 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedContents do end end - describe "[account favorited jobs]" do + describe "[account published jobs]" do @query """ query($userId: ID!, $filter: PagedFilter!) { publishedJobs(userId: $userId, filter: $filter) { @@ -93,7 +93,7 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedContents do end end - describe "[account favorited videos]" do + describe "[account published videos]" do @query """ query($userId: ID!, $filter: PagedFilter!) { publishedVideos(userId: $userId, filter: $filter) { @@ -132,7 +132,7 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedContents do end end - describe "[account favorited repos]" do + describe "[account published repos]" do @query """ query($userId: ID!, $filter: PagedFilter!) { publishedRepos(userId: $userId, filter: $filter) { From 68488e74b08609692112bb00d2b5b44e9b626471 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 16 Oct 2018 16:22:21 +0800 Subject: [PATCH 073/129] refactor(comments): extract comments macro --- .../schema/cms/cms_types.ex | 80 +------------------ lib/mastani_server_web/schema/utils/helper.ex | 78 ++++++++++++++++++ 2 files changed, 82 insertions(+), 76 deletions(-) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 6541e791f..514f55b16 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -17,82 +17,6 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:id, :id) end - object :comment do - field(:id, :id) - field(:body, :string) - field(:floor, :integer) - field(:author, :user, resolve: dataloader(CMS, :author)) - - field :reply_to, :comment do - resolve(dataloader(CMS, :reply_to)) - end - - field :likes, list_of(:user) do - arg(:filter, :members_filter) - - middleware(M.PageSizeProof) - resolve(dataloader(CMS, :likes)) - end - - field :likes_count, :integer do - arg(:count, :count_type, default_value: :count) - - resolve(dataloader(CMS, :likes)) - middleware(M.ConvertToInt) - end - - field :viewer_has_liked, :boolean do - arg(:viewer_did, :viewer_did_type, default_value: :viewer_did) - - middleware(M.Authorize, :login) - # put current user into dataloader's args - middleware(M.PutCurrentUser) - resolve(dataloader(CMS, :likes)) - middleware(M.ViewerDidConvert) - end - - field :dislikes, list_of(:user) do - arg(:filter, :members_filter) - - middleware(M.PageSizeProof) - resolve(dataloader(CMS, :dislikes)) - end - - field :viewer_has_disliked, :boolean do - arg(:viewer_did, :viewer_did_type, default_value: :viewer_did) - - middleware(M.Authorize, :login) - # put current user into dataloader's args - middleware(M.PutCurrentUser) - resolve(dataloader(CMS, :dislikes)) - middleware(M.ViewerDidConvert) - end - - field :dislikes_count, :integer do - arg(:count, :count_type, default_value: :count) - - resolve(dataloader(CMS, :dislikes)) - middleware(M.ConvertToInt) - end - - field :replies, list_of(:comment) do - arg(:filter, :members_filter) - - middleware(M.ForceLoader) - middleware(M.PageSizeProof) - resolve(dataloader(CMS, :replies)) - end - - field :replies_count, :integer do - arg(:count, :count_type, default_value: :count) - - resolve(dataloader(CMS, :replies)) - middleware(M.ConvertToInt) - end - - timestamp_fields() - end - object :post do interface(:article) field(:id, :id) @@ -420,6 +344,10 @@ defmodule MastaniServerWeb.Schema.CMS.Types do timestamp_fields() end + object :comment do + comments_fields() + end + object :paged_categories do field(:entries, list_of(:category)) pagination_fields() diff --git a/lib/mastani_server_web/schema/utils/helper.ex b/lib/mastani_server_web/schema/utils/helper.ex index 0746db6db..fd24b7842 100644 --- a/lib/mastani_server_web/schema/utils/helper.ex +++ b/lib/mastani_server_web/schema/utils/helper.ex @@ -135,4 +135,82 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do end end end + + defmacro comments_fields do + quote do + field(:id, :id) + field(:body, :string) + field(:floor, :integer) + field(:author, :user, resolve: dataloader(CMS, :author)) + + field :reply_to, :comment do + resolve(dataloader(CMS, :reply_to)) + end + + field :likes, list_of(:user) do + arg(:filter, :members_filter) + + middleware(M.PageSizeProof) + resolve(dataloader(CMS, :likes)) + end + + field :likes_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(CMS, :likes)) + middleware(M.ConvertToInt) + end + + field :viewer_has_liked, :boolean do + arg(:viewer_did, :viewer_did_type, default_value: :viewer_did) + + middleware(M.Authorize, :login) + # put current user into dataloader's args + middleware(M.PutCurrentUser) + resolve(dataloader(CMS, :likes)) + middleware(M.ViewerDidConvert) + end + + field :dislikes, list_of(:user) do + arg(:filter, :members_filter) + + middleware(M.PageSizeProof) + resolve(dataloader(CMS, :dislikes)) + end + + field :viewer_has_disliked, :boolean do + arg(:viewer_did, :viewer_did_type, default_value: :viewer_did) + + middleware(M.Authorize, :login) + # put current user into dataloader's args + middleware(M.PutCurrentUser) + resolve(dataloader(CMS, :dislikes)) + middleware(M.ViewerDidConvert) + end + + field :dislikes_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(CMS, :dislikes)) + middleware(M.ConvertToInt) + end + + field :replies, list_of(:comment) do + arg(:filter, :members_filter) + + middleware(M.ForceLoader) + middleware(M.PageSizeProof) + resolve(dataloader(CMS, :replies)) + end + + field :replies_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(CMS, :replies)) + middleware(M.ConvertToInt) + end + + timestamp_fields() + end + end end From 7ec06a9853b254949fc7b94eb12fa16cad2562c6 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 16 Oct 2018 16:23:02 +0800 Subject: [PATCH 074/129] chore: cleanup warnings --- .../query/accounts/published_comments_test.exs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/mastani_server_web/query/accounts/published_comments_test.exs b/test/mastani_server_web/query/accounts/published_comments_test.exs index f495efbfb..5bbb726cc 100644 --- a/test/mastani_server_web/query/accounts/published_comments_test.exs +++ b/test/mastani_server_web/query/accounts/published_comments_test.exs @@ -7,12 +7,11 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do setup do {:ok, user} = db_insert(:user) - {:ok, community} = db_insert(:community) guest_conn = simu_conn(:guest) user_conn = simu_conn(:user, user) - {:ok, ~m(guest_conn user_conn user community)a} + {:ok, ~m(guest_conn user_conn user)a} end describe "[account published comments on post]" do @@ -33,7 +32,7 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do } } """ - test "user can get paged published comments on post", ~m(guest_conn user community)a do + test "user can get paged published comments on post", ~m(guest_conn user)a do {:ok, post} = db_insert(:post) pub_comments = @@ -74,7 +73,7 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do } } """ - test "user can get paged published comments on job", ~m(guest_conn user community)a do + test "user can get paged published comments on job", ~m(guest_conn user)a do {:ok, job} = db_insert(:job) pub_comments = @@ -115,7 +114,7 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do } } """ - test "user can get paged published comments on video", ~m(guest_conn user community)a do + test "user can get paged published comments on video", ~m(guest_conn user)a do {:ok, video} = db_insert(:video) pub_comments = @@ -156,7 +155,7 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do } } """ - test "user can get paged published comments on repo", ~m(guest_conn user community)a do + test "user can get paged published comments on repo", ~m(guest_conn user)a do {:ok, repo} = db_insert(:repo) pub_comments = From eac987abf18a0b04a75e8ad394c8d7410a548b64 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 16 Oct 2018 16:41:22 +0800 Subject: [PATCH 075/129] refactor(published comments): support parent content info --- .../schema/account/account_queries.ex | 34 +++++++++++- .../schema/cms/cms_types.ex | 40 ++++++++++++++ .../accounts/published_comments_test.exs | 52 +++++++++++++------ 3 files changed, 108 insertions(+), 18 deletions(-) diff --git a/lib/mastani_server_web/schema/account/account_queries.ex b/lib/mastani_server_web/schema/account/account_queries.ex index b1b8b31d0..1ad44b791 100644 --- a/lib/mastani_server_web/schema/account/account_queries.ex +++ b/lib/mastani_server_web/schema/account/account_queries.ex @@ -172,10 +172,40 @@ defmodule MastaniServerWeb.Schema.Account.Queries do end @desc "get paged published comments on post" - field :published_comments, :paged_comments do + field :published_post_comments, :paged_post_comments do arg(:user_id, non_null(:id)) arg(:filter, non_null(:paged_filter)) - arg(:thread, :commentable_thread, default_value: :post) + arg(:thread, :post_thread, default_value: :post) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.published_comments/3) + end + + @desc "get paged published comments on job" + field :published_job_comments, :paged_job_comments do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:thread, :job_thread, default_value: :job) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.published_comments/3) + end + + @desc "get paged published comments on video" + field :published_video_comments, :paged_video_comments do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:thread, :video_thread, default_value: :video) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.published_comments/3) + end + + @desc "get paged published comments on repo" + field :published_repo_comments, :paged_repo_comments do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:thread, :repo_thread, default_value: :repo) middleware(M.PageSizeProof) resolve(&R.Accounts.published_comments/3) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 514f55b16..334d896ee 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -348,6 +348,26 @@ defmodule MastaniServerWeb.Schema.CMS.Types do comments_fields() end + object :post_comment do + comments_fields() + field(:post, :post, resolve: dataloader(CMS, :post)) + end + + object :job_comment do + comments_fields() + field(:job, :job, resolve: dataloader(CMS, :job)) + end + + object :video_comment do + comments_fields() + field(:video, :video, resolve: dataloader(CMS, :video)) + end + + object :repo_comment do + comments_fields() + field(:repo, :repo, resolve: dataloader(CMS, :repo)) + end + object :paged_categories do field(:entries, list_of(:category)) pagination_fields() @@ -378,6 +398,26 @@ defmodule MastaniServerWeb.Schema.CMS.Types do pagination_fields() end + object :paged_post_comments do + field(:entries, list_of(:post_comment)) + pagination_fields() + end + + object :paged_job_comments do + field(:entries, list_of(:job_comment)) + pagination_fields() + end + + object :paged_video_comments do + field(:entries, list_of(:video_comment)) + pagination_fields() + end + + object :paged_repo_comments do + field(:entries, list_of(:repo_comment)) + pagination_fields() + end + object :paged_communities do field(:entries, list_of(:community)) pagination_fields() diff --git a/test/mastani_server_web/query/accounts/published_comments_test.exs b/test/mastani_server_web/query/accounts/published_comments_test.exs index 5bbb726cc..0adfdc79e 100644 --- a/test/mastani_server_web/query/accounts/published_comments_test.exs +++ b/test/mastani_server_web/query/accounts/published_comments_test.exs @@ -16,14 +16,18 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do describe "[account published comments on post]" do @query """ - query($userId: ID!, $thread: CommentableThread, $filter: PagedFilter!) { - publishedComments(userId: $userId, thread: $thread, filter: $filter) { + query($userId: ID!, $filter: PagedFilter!) { + publishedPostComments(userId: $userId, filter: $filter) { entries { id body author { id } + post { + id + title + } } totalPages totalCount @@ -44,12 +48,13 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) |> to_string - variables = %{userId: user.id, thread: "POST", filter: %{page: 1, size: 20}} - results = guest_conn |> query_result(@query, variables, "publishedComments") + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedPostComments") assert results |> is_valid_pagination? assert results["totalCount"] == @publish_count + assert results["entries"] |> Enum.all?(&(&1["post"]["id"] == to_string(post.id))) assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) assert results["entries"] |> Enum.any?(&(&1["id"] == random_comment_id)) end @@ -57,14 +62,18 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do describe "[account published comments on job]" do @query """ - query($userId: ID!, $thread: CommentableThread, $filter: PagedFilter!) { - publishedComments(userId: $userId, thread: $thread, filter: $filter) { + query($userId: ID!, $filter: PagedFilter!) { + publishedJobComments(userId: $userId, filter: $filter) { entries { id body author { id } + job { + id + title + } } totalPages totalCount @@ -85,12 +94,13 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) |> to_string - variables = %{userId: user.id, thread: "JOB", filter: %{page: 1, size: 20}} - results = guest_conn |> query_result(@query, variables, "publishedComments") + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedJobComments") assert results |> is_valid_pagination? assert results["totalCount"] == @publish_count + assert results["entries"] |> Enum.all?(&(&1["job"]["id"] == to_string(job.id))) assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) assert results["entries"] |> Enum.any?(&(&1["id"] == random_comment_id)) end @@ -98,14 +108,18 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do describe "[account published comments on video]" do @query """ - query($userId: ID!, $thread: CommentableThread, $filter: PagedFilter!) { - publishedComments(userId: $userId, thread: $thread, filter: $filter) { + query($userId: ID!, $filter: PagedFilter!) { + publishedVideoComments(userId: $userId, filter: $filter) { entries { id body author { id } + video { + id + title + } } totalPages totalCount @@ -126,12 +140,13 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) |> to_string - variables = %{userId: user.id, thread: "VIDEO", filter: %{page: 1, size: 20}} - results = guest_conn |> query_result(@query, variables, "publishedComments") + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedVideoComments") assert results |> is_valid_pagination? assert results["totalCount"] == @publish_count + assert results["entries"] |> Enum.all?(&(&1["video"]["id"] == to_string(video.id))) assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) assert results["entries"] |> Enum.any?(&(&1["id"] == random_comment_id)) end @@ -139,14 +154,18 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do describe "[account published comments on repo]" do @query """ - query($userId: ID!, $thread: CommentableThread, $filter: PagedFilter!) { - publishedComments(userId: $userId, thread: $thread, filter: $filter) { + query($userId: ID!, $filter: PagedFilter!) { + publishedRepoComments(userId: $userId, filter: $filter) { entries { id body author { id } + repo { + id + title + } } totalPages totalCount @@ -167,12 +186,13 @@ defmodule MastaniServer.Test.Query.Accounts.PublishedComments do random_comment_id = pub_comments |> Enum.random() |> Map.get(:id) |> to_string - variables = %{userId: user.id, thread: "REPO", filter: %{page: 1, size: 20}} - results = guest_conn |> query_result(@query, variables, "publishedComments") + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "publishedRepoComments") assert results |> is_valid_pagination? assert results["totalCount"] == @publish_count + assert results["entries"] |> Enum.all?(&(&1["repo"]["id"] == to_string(repo.id))) assert results["entries"] |> Enum.all?(&(&1["author"]["id"] == to_string(user.id))) assert results["entries"] |> Enum.any?(&(&1["id"] == random_comment_id)) end From 49c459063f04c7213fd43b1e6adce977323c7948 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 17 Oct 2018 13:39:57 +0800 Subject: [PATCH 076/129] fix(cms passport): return empty instead of nil --- .../cms/delegates/passport_curd.ex | 10 ++++++++-- .../resolvers/accounts_resolver.ex | 8 ++------ test/mastani_server/cms/cms_passport_test.exs | 2 +- .../query/accounts/account_test.exs | 16 +++++++++++++--- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/mastani_server/cms/delegates/passport_curd.ex b/lib/mastani_server/cms/delegates/passport_curd.ex index 8f5b0d78d..64ddb9603 100644 --- a/lib/mastani_server/cms/delegates/passport_curd.ex +++ b/lib/mastani_server/cms/delegates/passport_curd.ex @@ -25,8 +25,14 @@ defmodule MastaniServer.CMS.Delegate.PassportCURD do return a user's passport in CMS context """ def get_passport(%Accounts.User{} = user) do - with {:ok, passport} <- ORM.find_by(UserPasport, user_id: user.id) do - {:ok, passport.rules} + with {:ok, _} <- ORM.find(Accounts.User, user.id) do + case ORM.find_by(UserPasport, user_id: user.id) do + {:ok, passport} -> + {:ok, passport.rules} + + {:error, error} -> + {:ok, %{}} + end end end diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 65e583332..ea0a4df44 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -210,12 +210,8 @@ defmodule MastaniServerWeb.Resolvers.Accounts do end def get_passport_string(root, _args, %{context: %{cur_user: _}}) do - case CMS.get_passport(%User{id: root.id}) do - {:ok, passport} -> - {:ok, Jason.encode!(passport)} - - {:error, _} -> - {:ok, nil} + with {:ok, passport} <- CMS.get_passport(%User{id: root.id}) do + {:ok, Jason.encode!(passport)} end end diff --git a/test/mastani_server/cms/cms_passport_test.exs b/test/mastani_server/cms/cms_passport_test.exs index 8da7847e0..d98dddab8 100644 --- a/test/mastani_server/cms/cms_passport_test.exs +++ b/test/mastani_server/cms/cms_passport_test.exs @@ -58,7 +58,7 @@ defmodule MastaniServer.Test.CMSPassport do end test "get a normal user's passport fails", ~m(user)a do - assert {:error, _} = CMS.get_passport(user) + assert {:ok, %{}} = CMS.get_passport(user) end test "get a non-exsit user's passport fails" do diff --git a/test/mastani_server_web/query/accounts/account_test.exs b/test/mastani_server_web/query/accounts/account_test.exs index c01dd5f19..cf7c4aa51 100644 --- a/test/mastani_server_web/query/accounts/account_test.exs +++ b/test/mastani_server_web/query/accounts/account_test.exs @@ -75,6 +75,16 @@ defmodule MastaniServer.Test.Query.Account.Basic do assert results["nickname"] == user.nickname assert results["educationBackgrounds"] == [] assert results["workBackgrounds"] == [] + assert results["cmsPassport"] == nil + end + + test "login newbie user can get own empty cms_passport", ~m(user)a do + user_conn = simu_conn(:user, user) + variables = %{id: user.id} + results = user_conn |> query_result(@query, variables, "user") + + assert results["cmsPassport"] == %{} + assert results["cmsPassportString"] == "{}" end @valid_rules %{ @@ -96,14 +106,14 @@ defmodule MastaniServer.Test.Query.Account.Basic do assert Map.equal?(Jason.decode!(results["cmsPassportString"]), @valid_rules) end - test "login user can get nil if cms_passport not exsit", ~m(user)a do + test "login user can get empty if cms_passport not exsit", ~m(user)a do user_conn = simu_conn(:user, user) variables = %{id: user.id} results = user_conn |> query_result(@query, variables, "user") - assert nil == results["cmsPassport"] - assert nil == results["cmsPassportString"] + assert %{} == results["cmsPassport"] + assert "{}" == results["cmsPassportString"] end @query """ From 8d62d88bda2bbb0131730a5703fb9687a6e96055 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 17 Oct 2018 14:56:38 +0800 Subject: [PATCH 077/129] feat(comments): add comemntsCount for job/video/repo --- lib/mastani_server/cms/utils/loader.ex | 23 ++++++++++++++-- .../schema/cms/cms_types.ex | 21 +++++++++++++++ .../query/cms/job_comment_test.exs | 27 +++++++++++++++++++ .../query/cms/repo_comment_test.exs | 25 +++++++++++++++++ .../query/cms/video_comment_test.exs | 25 +++++++++++++++++ 5 files changed, 119 insertions(+), 2 deletions(-) diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index d893f278f..9ce076275 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -24,18 +24,19 @@ defmodule MastaniServer.CMS.Utils.Loader do # Job, JobFavorite, # JobStar, + JobComment, JobCommentReply, JobCommentDislike, JobCommentLike, - # job comment - # JobComment, # Video VideoFavorite, VideoStar, + VideoComment, VideoCommentReply, VideoCommentDislike, VideoCommentLike, # repo + RepoComment, RepoCommentReply, RepoCommentLike, RepoCommentDislike @@ -309,6 +310,12 @@ defmodule MastaniServer.CMS.Utils.Loader do end # ---- job comments ------ + def query({"jobs_comments", JobComment}, %{count: _}) do + JobComment + |> group_by([c], c.job_id) + |> select([c], count(c.id)) + end + def query({"jobs_comments_replies", JobCommentReply}, %{count: _}) do JobCommentReply |> group_by([c], c.job_comment_id) @@ -360,6 +367,12 @@ defmodule MastaniServer.CMS.Utils.Loader do # ---- job ------ # ---- video comments ------ + def query({"videos_comments", VideoComment}, %{count: _}) do + VideoComment + |> group_by([c], c.video_id) + |> select([c], count(c.id)) + end + def query({"videos_comments_replies", VideoCommentReply}, %{count: _}) do VideoCommentReply |> group_by([c], c.video_comment_id) @@ -413,6 +426,12 @@ defmodule MastaniServer.CMS.Utils.Loader do # ---- video ------ # --- repo comments ------ + def query({"repos_comments", RepoComment}, %{count: _}) do + RepoComment + |> group_by([c], c.repo_id) + |> select([c], count(c.id)) + end + def query({"repos_comments_replies", RepoCommentReply}, %{count: _}) do RepoCommentReply |> group_by([c], c.repo_comment_id) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 334d896ee..711ad5458 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -121,6 +121,13 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + field :comments_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(CMS, :comments)) + middleware(M.ConvertToInt) + end + favorite_fields(:video) star_fields(:video) timestamp_fields() @@ -172,6 +179,13 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + field :comments_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(CMS, :comments)) + middleware(M.ConvertToInt) + end + timestamp_fields() end @@ -198,6 +212,13 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + field :comments_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(CMS, :comments)) + middleware(M.ConvertToInt) + end + favorite_fields(:job) timestamp_fields() end diff --git a/test/mastani_server_web/query/cms/job_comment_test.exs b/test/mastani_server_web/query/cms/job_comment_test.exs index 06aee3a6f..5454ea0bc 100644 --- a/test/mastani_server_web/query/cms/job_comment_test.exs +++ b/test/mastani_server_web/query/cms/job_comment_test.exs @@ -14,7 +14,33 @@ defmodule MastaniServer.Test.Query.JobComment do end # TODO: user can get specific user's replies :list_replies + describe "[job comment]" do + @query """ + query($filter: PagedArticleFilter) { + pagedJobs(filter: $filter) { + entries { + id + title + commentsCount + } + totalCount + } + } + """ + test "can get comments info in paged jobs", ~m(user guest_conn)a do + body = "this is a test comment" + + {:ok, community} = db_insert(:community) + {:ok, job} = CMS.create_content(community, :job, mock_attrs(:job), user) + {:ok, _comment} = CMS.create_comment(:job, job.id, body, user) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedJobs") + + assert results["entries"] |> List.first() |> Map.get("commentsCount") == 1 + end + @query """ query($thread: CmsThread, $id: ID!, $filter: CommentsFilter!) { pagedComments(thread: $thread, id: $id, filter: $filter) { @@ -23,6 +49,7 @@ defmodule MastaniServer.Test.Query.JobComment do body likesCount dislikesCount + commentsCount } totalPages totalCount diff --git a/test/mastani_server_web/query/cms/repo_comment_test.exs b/test/mastani_server_web/query/cms/repo_comment_test.exs index 798b902de..09549ae92 100644 --- a/test/mastani_server_web/query/cms/repo_comment_test.exs +++ b/test/mastani_server_web/query/cms/repo_comment_test.exs @@ -15,6 +15,31 @@ defmodule MastaniServer.Test.Query.RepoComment do # TODO: user can get specific user's replies :list_replies describe "[repo comment]" do + @query """ + query($filter: PagedArticleFilter) { + pagedRepos(filter: $filter) { + entries { + id + title + commentsCount + } + totalCount + } + } + """ + test "can get comments info in paged repos", ~m(user guest_conn)a do + body = "this is a test comment" + + {:ok, community} = db_insert(:community) + {:ok, repo} = CMS.create_content(community, :repo, mock_attrs(:repo), user) + {:ok, _comment} = CMS.create_comment(:repo, repo.id, body, user) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedRepos") + + assert results["entries"] |> List.first() |> Map.get("commentsCount") == 1 + end + @query """ query($thread: CmsThread, $id: ID!, $filter: CommentsFilter!) { pagedComments(thread: $thread, id: $id, filter: $filter) { diff --git a/test/mastani_server_web/query/cms/video_comment_test.exs b/test/mastani_server_web/query/cms/video_comment_test.exs index ddb72697d..f580a3b2a 100644 --- a/test/mastani_server_web/query/cms/video_comment_test.exs +++ b/test/mastani_server_web/query/cms/video_comment_test.exs @@ -15,6 +15,31 @@ defmodule MastaniServer.Test.Query.VideoComment do # TODO: user can get specific user's replies :list_replies describe "[video comment]" do + @query """ + query($filter: PagedArticleFilter) { + pagedVideos(filter: $filter) { + entries { + id + title + commentsCount + } + totalCount + } + } + """ + test "can get comments info in paged videos", ~m(user guest_conn)a do + body = "this is a test comment" + + {:ok, community} = db_insert(:community) + {:ok, video} = CMS.create_content(community, :video, mock_attrs(:video), user) + {:ok, _comment} = CMS.create_comment(:video, video.id, body, user) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedVideos") + + assert results["entries"] |> List.first() |> Map.get("commentsCount") == 1 + end + @query """ query($thread: CmsThread, $id: ID!, $filter: CommentsFilter!) { pagedComments(thread: $thread, id: $id, filter: $filter) { From 5513c768812fcd892849fd9db230692be949e32c Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 20 Oct 2018 08:31:43 +0800 Subject: [PATCH 078/129] fix: missing tags --- lib/mastani_server_web/schema/cms/cms_types.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 711ad5458..0ba428b06 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -118,7 +118,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:pin, :boolean) field(:trash, :boolean) - # field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) + field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) field :comments_count, :integer do @@ -151,6 +151,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:owner_name, :string) field(:owner_url, :string) field(:repo_url, :string) + field(:author, :user, resolve: dataloader(CMS, :author)) field(:desc, :string) field(:homepage_url, :string) @@ -168,7 +169,6 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:contributors, list_of(:repo_contributor)) - field(:author, :user, resolve: dataloader(CMS, :author)) field(:views, :integer) field(:pin, :boolean) field(:trash, :boolean) @@ -176,7 +176,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # field(:pin, :boolean) # field(:trash, :boolean) - # field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) + field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) field :comments_count, :integer do From 10ce7d0be93c58ab0dae430f11a32a18a362a2d4 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 20 Oct 2018 10:03:48 +0800 Subject: [PATCH 079/129] feat(reactions): repo favorite with basic test --- lib/mastani_server/cms/repo.ex | 2 + lib/mastani_server/cms/repo_favorite.ex | 30 +++++++++ lib/mastani_server/cms/utils/matcher.ex | 4 ++ lib/mastani_server_web/schema/cms/cms_misc.ex | 2 + .../20181020014210_add_repo_favorites.exs | 16 +++++ .../cms/repo_reactions_test.exs | 33 ++++++++++ .../mutation/cms/repo_reaction_test.exs | 65 +++++++++++++++++++ 7 files changed, 152 insertions(+) create mode 100644 lib/mastani_server/cms/repo_favorite.ex create mode 100644 priv/repo/migrations/20181020014210_add_repo_favorites.exs create mode 100644 test/mastani_server/cms/repo_reactions_test.exs create mode 100644 test/mastani_server_web/mutation/cms/repo_reaction_test.exs diff --git a/lib/mastani_server/cms/repo.ex b/lib/mastani_server/cms/repo.ex index aedd6da30..4889c9929 100644 --- a/lib/mastani_server/cms/repo.ex +++ b/lib/mastani_server/cms/repo.ex @@ -10,6 +10,7 @@ defmodule MastaniServer.CMS.Repo do Community, RepoComment, RepoContributor, + RepoFavorite, RepoLang, RepoCommunityFlag, Tag @@ -52,6 +53,7 @@ defmodule MastaniServer.CMS.Repo do field(:last_fetch_time, :utc_datetime) has_many(:comments, {"repos_comments", RepoComment}) + has_many(:favorites, {"repos_favorites", RepoFavorite}) many_to_many( :tags, diff --git a/lib/mastani_server/cms/repo_favorite.ex b/lib/mastani_server/cms/repo_favorite.ex new file mode 100644 index 000000000..fea48c8a3 --- /dev/null +++ b/lib/mastani_server/cms/repo_favorite.ex @@ -0,0 +1,30 @@ +defmodule MastaniServer.CMS.RepoFavorite do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.Repo + + @required_fields ~w(user_id repo_id)a + @optional_fields ~w(category_id)a + + @type t :: %RepoFavorite{} + schema "repos_favorites" do + belongs_to(:user, Accounts.User, foreign_key: :user_id) + belongs_to(:repo, Repo, foreign_key: :repo_id) + + belongs_to(:category, Accounts.FavoriteCategory) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%RepoFavorite{} = repo_favorite, attrs) do + repo_favorite + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_required(@required_fields) + |> unique_constraint(:user_id, name: :repos_favorites_user_id_repo_id_index) + end +end diff --git a/lib/mastani_server/cms/utils/matcher.ex b/lib/mastani_server/cms/utils/matcher.ex index 5fdf353be..d35a73fbd 100644 --- a/lib/mastani_server/cms/utils/matcher.ex +++ b/lib/mastani_server/cms/utils/matcher.ex @@ -15,6 +15,7 @@ defmodule MastaniServer.CMS.Utils.Matcher do PostFavorite, JobFavorite, VideoFavorite, + RepoFavorite, PostStar, JobStar, VideoStar, @@ -133,6 +134,9 @@ defmodule MastaniServer.CMS.Utils.Matcher do def match_action(:repo, :community), do: {:ok, %{target: Repo, reactor: Community, flag: RepoCommunityFlag}} + def match_action(:repo, :favorite), + do: {:ok, %{target: Repo, reactor: RepoFavorite, preload: :user}} + def match_action(:repo, :comment), do: {:ok, %{target: Repo, reactor: RepoComment, preload: :author}} diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index 0d0f55ebd..16b6a4364 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -35,6 +35,7 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do enum :reactable_action do value(:star) + value(:favorite) # value(:watch) end @@ -42,6 +43,7 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do value(:post) value(:job) value(:video) + value(:repo) end enum :cms_comment do diff --git a/priv/repo/migrations/20181020014210_add_repo_favorites.exs b/priv/repo/migrations/20181020014210_add_repo_favorites.exs new file mode 100644 index 000000000..b5d0d6881 --- /dev/null +++ b/priv/repo/migrations/20181020014210_add_repo_favorites.exs @@ -0,0 +1,16 @@ +defmodule MastaniServer.Repo.Migrations.AddRepoFavorites do + use Ecto.Migration + + def change do + create table(:repos_favorites) do + add(:user_id, references(:users, on_delete: :delete_all), null: false) + add(:repo_id, references(:cms_repos, on_delete: :delete_all), null: false) + add(:category_id, references(:favorite_categories, on_delete: :delete_all)) + + timestamps() + end + + create(index(:repos_favorites, [:category_id])) + create(unique_index(:repos_favorites, [:user_id, :repo_id])) + end +end diff --git a/test/mastani_server/cms/repo_reactions_test.exs b/test/mastani_server/cms/repo_reactions_test.exs new file mode 100644 index 000000000..37b9a6925 --- /dev/null +++ b/test/mastani_server/cms/repo_reactions_test.exs @@ -0,0 +1,33 @@ +defmodule MastaniServer.Test.RepoReactions do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, user} = db_insert(:user) + {:ok, community} = db_insert(:community) + + repo_attrs = mock_attrs(:repo, %{community_id: community.id}) + + {:ok, ~m(user community repo_attrs)a} + end + + describe "[cms repo star/favorite reaction]" do + @tag :wip + test "favorite and undo favorite reaction to repo", ~m(user community repo_attrs)a do + {:ok, repo} = CMS.create_content(community, :repo, repo_attrs, user) + + {:ok, _} = CMS.reaction(:repo, :favorite, repo.id, user) + {:ok, reaction_users} = CMS.reaction_users(:repo, :favorite, repo.id, %{page: 1, size: 1}) + reaction_users = reaction_users |> Map.get(:entries) + assert 1 == reaction_users |> Enum.filter(fn ruser -> user.id == ruser.id end) |> length + + # undo test + {:ok, _} = CMS.undo_reaction(:repo, :favorite, repo.id, user) + {:ok, reaction_users2} = CMS.reaction_users(:repo, :favorite, repo.id, %{page: 1, size: 1}) + reaction_users2 = reaction_users2 |> Map.get(:entries) + + assert 0 == reaction_users2 |> Enum.filter(fn ruser -> user.id == ruser.id end) |> length + end + end +end diff --git a/test/mastani_server_web/mutation/cms/repo_reaction_test.exs b/test/mastani_server_web/mutation/cms/repo_reaction_test.exs new file mode 100644 index 000000000..462981d4c --- /dev/null +++ b/test/mastani_server_web/mutation/cms/repo_reaction_test.exs @@ -0,0 +1,65 @@ +defmodule MastaniServer.Test.Mutation.RepoReaction do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, repo} = db_insert(:repo) + {:ok, user} = db_insert(:user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(user_conn guest_conn repo user)a} + end + + describe "[repo favorite]" do + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + reaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can star a repo", ~m(user_conn repo)a do + variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} + created = user_conn |> mutation_result(@query, variables, "reaction") + + assert created["id"] == to_string(repo.id) + end + + @tag :wip + test "unauth user star a repo fails", ~m(guest_conn repo)a do + variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + undoReaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + @tag :wip + test "login user can undo star a repo", ~m(user_conn repo user)a do + {:ok, _} = CMS.reaction(:repo, :favorite, repo.id, user) + + variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} + updated = user_conn |> mutation_result(@query, variables, "undoReaction") + + assert updated["id"] == to_string(repo.id) + end + + @tag :wip + test "unauth user undo star a repo fails", ~m(guest_conn repo)a do + variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + end +end From b97ae50289939a95d34fd8aec3ecd85b04af6aef Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 20 Oct 2018 10:34:43 +0800 Subject: [PATCH 080/129] test(reactions): more test & doc for repo-favorite --- .../accounts/delegates/favorite_category.ex | 10 +- lib/mastani_server_web/schema/cms/cms_misc.ex | 2 +- .../schema/cms/cms_types.ex | 4 + .../accounts/favorite_category_test.exs | 117 +++++++++++++++++- .../mutation/cms/repo_reaction_test.exs | 65 ---------- .../query/cms/job_comment_test.exs | 2 +- 6 files changed, 127 insertions(+), 73 deletions(-) delete mode 100644 test/mastani_server_web/mutation/cms/repo_reaction_test.exs diff --git a/lib/mastani_server/accounts/delegates/favorite_category.ex b/lib/mastani_server/accounts/delegates/favorite_category.ex index dcaaf9ade..7bc4875f7 100644 --- a/lib/mastani_server/accounts/delegates/favorite_category.ex +++ b/lib/mastani_server/accounts/delegates/favorite_category.ex @@ -16,7 +16,7 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do alias MastaniServer.Accounts.{FavoriteCategory, User} alias MastaniServer.{CMS, Repo} - alias CMS.{PostFavorite, JobFavorite, VideoFavorite} + alias CMS.{PostFavorite, JobFavorite, VideoFavorite, RepoFavorite} alias Ecto.Multi @@ -51,10 +51,13 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do {:ok, post_author_ids} = affected_author_ids(:post, CMS.PostFavorite, category) {:ok, job_author_ids} = affected_author_ids(:job, CMS.JobFavorite, category) {:ok, video_author_ids} = affected_author_ids(:video, CMS.VideoFavorite, category) + {:ok, repo_author_ids} = affected_author_ids(:repo, CMS.RepoFavorite, category) # author_ids_list = count_words(total_author_ids) |> Map.to_list author_ids_list = - (post_author_ids ++ job_author_ids ++ video_author_ids) |> count_words |> Map.to_list() + (post_author_ids ++ job_author_ids ++ video_author_ids ++ repo_author_ids) + |> count_words + |> Map.to_list() # NOTE: if the contents have too many unique authors, it may be crash the server # so limit size to 20 unique authors @@ -234,6 +237,9 @@ defmodule MastaniServer.Accounts.Delegate.FavoriteCategory do defp find_content_favorite(:video, content_id, user_id), do: VideoFavorite |> ORM.find_by(%{video_id: content_id, user_id: user_id}) + defp find_content_favorite(:repo, content_id, user_id), + do: RepoFavorite |> ORM.find_by(%{repo_id: content_id, user_id: user_id}) + defp check_dup_category(content, category) do case content.category_id !== category.id do true -> {:ok, content} diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index 16b6a4364..d72f1abf9 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -35,7 +35,7 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do enum :reactable_action do value(:star) - value(:favorite) + # value(:favorite) # value(:watch) end diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 0ba428b06..5c87d7f27 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -42,6 +42,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do resolve(dataloader(CMS, :comments)) end + @dec "total comments of the post" field :comments_count, :integer do arg(:count, :count_type, default_value: :count) @@ -121,6 +122,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + @dec "total comments of the video" field :comments_count, :integer do arg(:count, :count_type, default_value: :count) @@ -179,6 +181,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + @dec "total comments of the repo" field :comments_count, :integer do arg(:count, :count_type, default_value: :count) @@ -212,6 +215,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + @dec "total comments of the job" field :comments_count, :integer do arg(:count, :count_type, default_value: :count) diff --git a/test/mastani_server_web/mutation/accounts/favorite_category_test.exs b/test/mastani_server_web/mutation/accounts/favorite_category_test.exs index 4de9c3cbe..177ced509 100644 --- a/test/mastani_server_web/mutation/accounts/favorite_category_test.exs +++ b/test/mastani_server_web/mutation/accounts/favorite_category_test.exs @@ -11,11 +11,12 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do {:ok, post} = db_insert(:post) {:ok, job} = db_insert(:job) {:ok, video} = db_insert(:video) + {:ok, repo} = db_insert(:repo) user_conn = simu_conn(:user, user) guest_conn = simu_conn(:guest) - {:ok, ~m(user_conn guest_conn user post job video)a} + {:ok, ~m(user_conn guest_conn user post job video repo)a} end describe "[Accounts FavoriteCategory CURD]" do @@ -158,11 +159,12 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do } } """ + @tag :wip test "user can put a post to favorites category", ~m(user user_conn post)a do test_category = "test category" {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) - variables = %{id: post.id, categoryId: category.id} + variables = %{id: post.id, thread: "POST", categoryId: category.id} created = user_conn |> mutation_result(@query, variables, "setFavorites") {:ok, found} = CMS.PostFavorite |> ORM.find_by(%{post_id: post.id, user_id: user.id}) @@ -174,6 +176,57 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do assert found.post_id == post.id end + @tag :wip + test "user can put a job to favorites category", ~m(user user_conn job)a do + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + + variables = %{id: job.id, thread: "JOB", categoryId: category.id} + created = user_conn |> mutation_result(@query, variables, "setFavorites") + {:ok, found} = CMS.JobFavorite |> ORM.find_by(%{job_id: job.id, user_id: user.id}) + + assert created["totalCount"] == 1 + assert created["lastUpdated"] != nil + + assert found.category_id == category.id + assert found.user_id == user.id + assert found.job_id == job.id + end + + @tag :wip + test "user can put a video to favorites category", ~m(user user_conn video)a do + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + + variables = %{id: video.id, thread: "VIDEO", categoryId: category.id} + created = user_conn |> mutation_result(@query, variables, "setFavorites") + {:ok, found} = CMS.VideoFavorite |> ORM.find_by(%{video_id: video.id, user_id: user.id}) + + assert created["totalCount"] == 1 + assert created["lastUpdated"] != nil + + assert found.category_id == category.id + assert found.user_id == user.id + assert found.video_id == video.id + end + + @tag :wip + test "user can put a repo to favorites category", ~m(user user_conn repo)a do + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + + variables = %{id: repo.id, thread: "REPO", categoryId: category.id} + created = user_conn |> mutation_result(@query, variables, "setFavorites") + {:ok, found} = CMS.RepoFavorite |> ORM.find_by(%{repo_id: repo.id, user_id: user.id}) + + assert created["totalCount"] == 1 + assert created["lastUpdated"] != nil + + assert found.category_id == category.id + assert found.user_id == user.id + assert found.repo_id == repo.id + end + @query """ mutation($id: ID!, $thread: CmsThread, $categoryId: ID!) { unsetFavorites(id: $id, thread: $thread, categoryId: $categoryId) { @@ -184,7 +237,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do } } """ - test "user can unset favorites category", ~m(user user_conn post)a do + test "user can unset a post to favorites category", ~m(user user_conn post)a do test_category = "test category" {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) {:ok, _favorite_category} = Accounts.set_favorites(user, :post, post.id, category.id) @@ -193,12 +246,68 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do assert category.total_count == 1 assert category.last_updated != nil - variables = %{id: post.id, categoryId: category.id} + variables = %{id: post.id, thread: "POST", categoryId: category.id} user_conn |> mutation_result(@query, variables, "unsetFavorites") {:ok, category} = Accounts.FavoriteCategory |> ORM.find(category.id) assert category.total_count == 0 assert {:error, _} = CMS.PostFavorite |> ORM.find_by(%{post_id: post.id, user_id: user.id}) end + + @tag :wip + test "user can unset a job to favorites category", ~m(user user_conn job)a do + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, _favorite_category} = Accounts.set_favorites(user, :job, job.id, category.id) + + {:ok, category} = Accounts.FavoriteCategory |> ORM.find(category.id) + assert category.total_count == 1 + assert category.last_updated != nil + + variables = %{id: job.id, thread: "JOB", categoryId: category.id} + user_conn |> mutation_result(@query, variables, "unsetFavorites") + + {:ok, category} = Accounts.FavoriteCategory |> ORM.find(category.id) + assert category.total_count == 0 + assert {:error, _} = CMS.JobFavorite |> ORM.find_by(%{job_id: job.id, user_id: user.id}) + end + + @tag :wip + test "user can unset a video to favorites category", ~m(user user_conn video)a do + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, _favorite_category} = Accounts.set_favorites(user, :video, video.id, category.id) + + {:ok, category} = Accounts.FavoriteCategory |> ORM.find(category.id) + assert category.total_count == 1 + assert category.last_updated != nil + + variables = %{id: video.id, thread: "VIDEO", categoryId: category.id} + user_conn |> mutation_result(@query, variables, "unsetFavorites") + + {:ok, category} = Accounts.FavoriteCategory |> ORM.find(category.id) + assert category.total_count == 0 + + assert {:error, _} = + CMS.VideoFavorite |> ORM.find_by(%{video_id: video.id, user_id: user.id}) + end + + @tag :wip + test "user can unset a repo to favorites category", ~m(user user_conn repo)a do + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, _favorite_category} = Accounts.set_favorites(user, :repo, repo.id, category.id) + + {:ok, category} = Accounts.FavoriteCategory |> ORM.find(category.id) + assert category.total_count == 1 + assert category.last_updated != nil + + variables = %{id: repo.id, thread: "REPO", categoryId: category.id} + user_conn |> mutation_result(@query, variables, "unsetFavorites") + + {:ok, category} = Accounts.FavoriteCategory |> ORM.find(category.id) + assert category.total_count == 0 + assert {:error, _} = CMS.RepoFavorite |> ORM.find_by(%{repo_id: repo.id, user_id: user.id}) + end end end diff --git a/test/mastani_server_web/mutation/cms/repo_reaction_test.exs b/test/mastani_server_web/mutation/cms/repo_reaction_test.exs deleted file mode 100644 index 462981d4c..000000000 --- a/test/mastani_server_web/mutation/cms/repo_reaction_test.exs +++ /dev/null @@ -1,65 +0,0 @@ -defmodule MastaniServer.Test.Mutation.RepoReaction do - use MastaniServer.TestTools - - alias MastaniServer.CMS - - setup do - {:ok, repo} = db_insert(:repo) - {:ok, user} = db_insert(:user) - - guest_conn = simu_conn(:guest) - user_conn = simu_conn(:user, user) - - {:ok, ~m(user_conn guest_conn repo user)a} - end - - describe "[repo favorite]" do - @query """ - mutation($id: ID!, $action: String!, $thread: CmsThread!) { - reaction(id: $id, action: $action, thread: $thread) { - id - } - } - """ - @tag :wip - test "login user can star a repo", ~m(user_conn repo)a do - variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} - created = user_conn |> mutation_result(@query, variables, "reaction") - - assert created["id"] == to_string(repo.id) - end - - @tag :wip - test "unauth user star a repo fails", ~m(guest_conn repo)a do - variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} - - assert guest_conn - |> mutation_get_error?(@query, variables, ecode(:account_login)) - end - - @query """ - mutation($id: ID!, $action: String!, $thread: CmsThread!) { - undoReaction(id: $id, action: $action, thread: $thread) { - id - } - } - """ - @tag :wip - test "login user can undo star a repo", ~m(user_conn repo user)a do - {:ok, _} = CMS.reaction(:repo, :favorite, repo.id, user) - - variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} - updated = user_conn |> mutation_result(@query, variables, "undoReaction") - - assert updated["id"] == to_string(repo.id) - end - - @tag :wip - test "unauth user undo star a repo fails", ~m(guest_conn repo)a do - variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} - - assert guest_conn - |> mutation_get_error?(@query, variables, ecode(:account_login)) - end - end -end diff --git a/test/mastani_server_web/query/cms/job_comment_test.exs b/test/mastani_server_web/query/cms/job_comment_test.exs index 5454ea0bc..ec359b34b 100644 --- a/test/mastani_server_web/query/cms/job_comment_test.exs +++ b/test/mastani_server_web/query/cms/job_comment_test.exs @@ -49,7 +49,6 @@ defmodule MastaniServer.Test.Query.JobComment do body likesCount dislikesCount - commentsCount } totalPages totalCount @@ -58,6 +57,7 @@ defmodule MastaniServer.Test.Query.JobComment do } } """ + @tag :wip2 test "guest user can get a paged comment", ~m(guest_conn job user)a do body = "test comment" From c20a17117bda587e9ed616646ce850980a9f2e72 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 20 Oct 2018 10:36:25 +0800 Subject: [PATCH 081/129] chore(cleanup): rm wip tags --- .../cms/repo_reactions_test.exs | 1 - .../accounts/favorite_category_test.exs | 7 --- .../mutation/cms/repo_reaction_test.exs | 61 +++++++++++++++++++ .../query/cms/job_comment_test.exs | 1 - 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 test/mastani_server_web/mutation/cms/repo_reaction_test.exs diff --git a/test/mastani_server/cms/repo_reactions_test.exs b/test/mastani_server/cms/repo_reactions_test.exs index 37b9a6925..b5a2bad53 100644 --- a/test/mastani_server/cms/repo_reactions_test.exs +++ b/test/mastani_server/cms/repo_reactions_test.exs @@ -13,7 +13,6 @@ defmodule MastaniServer.Test.RepoReactions do end describe "[cms repo star/favorite reaction]" do - @tag :wip test "favorite and undo favorite reaction to repo", ~m(user community repo_attrs)a do {:ok, repo} = CMS.create_content(community, :repo, repo_attrs, user) diff --git a/test/mastani_server_web/mutation/accounts/favorite_category_test.exs b/test/mastani_server_web/mutation/accounts/favorite_category_test.exs index 177ced509..de517c26a 100644 --- a/test/mastani_server_web/mutation/accounts/favorite_category_test.exs +++ b/test/mastani_server_web/mutation/accounts/favorite_category_test.exs @@ -159,7 +159,6 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do } } """ - @tag :wip test "user can put a post to favorites category", ~m(user user_conn post)a do test_category = "test category" {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) @@ -176,7 +175,6 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do assert found.post_id == post.id end - @tag :wip test "user can put a job to favorites category", ~m(user user_conn job)a do test_category = "test category" {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) @@ -193,7 +191,6 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do assert found.job_id == job.id end - @tag :wip test "user can put a video to favorites category", ~m(user user_conn video)a do test_category = "test category" {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) @@ -210,7 +207,6 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do assert found.video_id == video.id end - @tag :wip test "user can put a repo to favorites category", ~m(user user_conn repo)a do test_category = "test category" {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) @@ -254,7 +250,6 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do assert {:error, _} = CMS.PostFavorite |> ORM.find_by(%{post_id: post.id, user_id: user.id}) end - @tag :wip test "user can unset a job to favorites category", ~m(user user_conn job)a do test_category = "test category" {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) @@ -272,7 +267,6 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do assert {:error, _} = CMS.JobFavorite |> ORM.find_by(%{job_id: job.id, user_id: user.id}) end - @tag :wip test "user can unset a video to favorites category", ~m(user user_conn video)a do test_category = "test category" {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) @@ -292,7 +286,6 @@ defmodule MastaniServer.Test.Mutation.Accounts.FavoriteCategory do CMS.VideoFavorite |> ORM.find_by(%{video_id: video.id, user_id: user.id}) end - @tag :wip test "user can unset a repo to favorites category", ~m(user user_conn repo)a do test_category = "test category" {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) diff --git a/test/mastani_server_web/mutation/cms/repo_reaction_test.exs b/test/mastani_server_web/mutation/cms/repo_reaction_test.exs new file mode 100644 index 000000000..4bcb455da --- /dev/null +++ b/test/mastani_server_web/mutation/cms/repo_reaction_test.exs @@ -0,0 +1,61 @@ +defmodule MastaniServer.Test.Mutation.RepoReaction do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + setup do + {:ok, repo} = db_insert(:repo) + {:ok, user} = db_insert(:user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(user_conn guest_conn repo user)a} + end + + describe "[repo favorite]" do + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + reaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + test "login user can star a repo", ~m(user_conn repo)a do + variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} + created = user_conn |> mutation_result(@query, variables, "reaction") + + assert created["id"] == to_string(repo.id) + end + + test "unauth user star a repo fails", ~m(guest_conn repo)a do + variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + + @query """ + mutation($id: ID!, $action: String!, $thread: CmsThread!) { + undoReaction(id: $id, action: $action, thread: $thread) { + id + } + } + """ + test "login user can undo star a repo", ~m(user_conn repo user)a do + {:ok, _} = CMS.reaction(:repo, :favorite, repo.id, user) + + variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} + updated = user_conn |> mutation_result(@query, variables, "undoReaction") + + assert updated["id"] == to_string(repo.id) + end + + test "unauth user undo star a repo fails", ~m(guest_conn repo)a do + variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} + + assert guest_conn + |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + end +end diff --git a/test/mastani_server_web/query/cms/job_comment_test.exs b/test/mastani_server_web/query/cms/job_comment_test.exs index ec359b34b..f22e31bae 100644 --- a/test/mastani_server_web/query/cms/job_comment_test.exs +++ b/test/mastani_server_web/query/cms/job_comment_test.exs @@ -57,7 +57,6 @@ defmodule MastaniServer.Test.Query.JobComment do } } """ - @tag :wip2 test "guest user can get a paged comment", ~m(guest_conn job user)a do body = "test comment" From cd668d340e4946290eb2230794a9d4a50a28e50d Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 20 Oct 2018 10:58:17 +0800 Subject: [PATCH 082/129] test(repo reaction): remove unneed tests on favroites --- .../mutation/cms/repo_reaction_test.exs | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 test/mastani_server_web/mutation/cms/repo_reaction_test.exs diff --git a/test/mastani_server_web/mutation/cms/repo_reaction_test.exs b/test/mastani_server_web/mutation/cms/repo_reaction_test.exs deleted file mode 100644 index 4bcb455da..000000000 --- a/test/mastani_server_web/mutation/cms/repo_reaction_test.exs +++ /dev/null @@ -1,61 +0,0 @@ -defmodule MastaniServer.Test.Mutation.RepoReaction do - use MastaniServer.TestTools - - alias MastaniServer.CMS - - setup do - {:ok, repo} = db_insert(:repo) - {:ok, user} = db_insert(:user) - - guest_conn = simu_conn(:guest) - user_conn = simu_conn(:user, user) - - {:ok, ~m(user_conn guest_conn repo user)a} - end - - describe "[repo favorite]" do - @query """ - mutation($id: ID!, $action: String!, $thread: CmsThread!) { - reaction(id: $id, action: $action, thread: $thread) { - id - } - } - """ - test "login user can star a repo", ~m(user_conn repo)a do - variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} - created = user_conn |> mutation_result(@query, variables, "reaction") - - assert created["id"] == to_string(repo.id) - end - - test "unauth user star a repo fails", ~m(guest_conn repo)a do - variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} - - assert guest_conn - |> mutation_get_error?(@query, variables, ecode(:account_login)) - end - - @query """ - mutation($id: ID!, $action: String!, $thread: CmsThread!) { - undoReaction(id: $id, action: $action, thread: $thread) { - id - } - } - """ - test "login user can undo star a repo", ~m(user_conn repo user)a do - {:ok, _} = CMS.reaction(:repo, :favorite, repo.id, user) - - variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} - updated = user_conn |> mutation_result(@query, variables, "undoReaction") - - assert updated["id"] == to_string(repo.id) - end - - test "unauth user undo star a repo fails", ~m(guest_conn repo)a do - variables = %{id: repo.id, thread: "REPO", action: "FAVORITE"} - - assert guest_conn - |> mutation_get_error?(@query, variables, ecode(:account_login)) - end - end -end From 752af9676fc8e8b7bf8abd70094ff27c5b0451d0 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 20 Oct 2018 14:34:20 +0800 Subject: [PATCH 083/129] feat(comments): comments participators for other jobs videos repos --- lib/mastani_server/cms/utils/loader.ex | 217 +++++++----------- .../middleware/cut_participators.ex | 21 ++ .../schema/cms/cms_types.ex | 131 ++++------- lib/mastani_server_web/schema/utils/helper.ex | 23 ++ mix.exs | 2 +- mix.lock | 2 +- .../query/cms/job_comment_test.exs | 70 +++++- .../query/cms/post_comment_test.exs | 31 +++ .../query/cms/repo_comment_test.exs | 69 ++++++ .../query/cms/video_comment_test.exs | 69 ++++++ 10 files changed, 409 insertions(+), 226 deletions(-) create mode 100644 lib/mastani_server_web/middleware/cut_participators.ex diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index 9ce076275..9e816761c 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -80,101 +80,7 @@ defmodule MastaniServer.CMS.Utils.Loader do for id <- post_ids, do: Map.get(results, id, [0]) end - def run_batch(PostComment, comment_query, :cp_users, post_ids, repo_opts) do - # IO.inspect(comment_query, label: "# run_batch # comment_query") - - sq = - from( - pc in comment_query, - join: a in assoc(pc, :author), - select: %{id: a.id, row_number: fragment("row_number() OVER (PARTITION BY author_id)")} - ) - - query = - from( - pc in comment_query, - join: s in subquery(sq), - on: s.id == pc.author_id, - where: s.row_number == 10, - select: {pc.post_id, s.id} - ) - - # query = comment_query - # |> join(:inner, [c], a in assoc(c, :author)) - # |> distinct([c, a], c.post_id) - # |> join(:inner_lateral, [c, a], u in fragment("SELECT * FROM users AS us WHERE us.id = ? LIMIT 1", a.id)) - # |> join(:inner_lateral, [c, a], u in fragment("SELECT * FROM users AS us WHERE us.id > ? LIMIT 1", 100)) - # |> select([c, a, u], {c.post_id, u.id, u.nickname}) - - results = - query - # |> IO.inspect(label: "before") - |> Repo.all(repo_opts) - # |> IO.inspect(label: "geting fuck") - |> bat_man() - - # results = - # comment_query - # |> join(:inner, [c], a in assoc(c, :author)) - # |> group_by([c, a], a.id) - # |> group_by([c, a], c.post_id) - # |> select([c, a], {c.post_id, a}) - # --------- - # |> join(:inner, [c], s in subquery(sq), on: s.id == c.post_id) - # |> join(:inner, [c], a in subquery(isubquery), c.post_id == 106) - # |> join(:inner_lateral, [c], a in fragment("SELECT * FROM users AS u WHERE u.id = ? LIMIT 3", c.post_id)) - # |> join(:inner_lateral, [c], a in fragment("SELECT * FROM users WHERE users.id > ? LIMIT 3", 100)) - # |> join(:inner_lateral, [c], a in fragment("SELECT * FROM posts_comments JOIN users ON users.id = ? LIMIT 2", c.author_id)) - # |> join(:inner_lateral, [c], a in fragment("SELECT * FROM posts_comments AS pc WHERE pc.author_id = ? LIMIT 2", 185)) - # |> join(:inner_lateral, [c], a in fragment("SELECT ROW_NUMBER() OVER (PARTITION BY ?) FROM posts_comments AS pc GROUP BY pc.post_id", c.post_id)) - # |> distinct([c, a], c.post_id) - # |> join(:inner_lateral, [c, a], x in fragment("SELECT * FROM posts_comments JOIN users ON users.id = posts_comments.author_id WHERE post_id = ? LIMIT 2", c.post_id)) - # |> join(:inner_lateral, [c, a], x in fragment("SELECT * FROM posts_comments JOIN users ON users.id = posts_comments.author_id LIMIT 3")) - # |> select([c,a,x], {c.post_id, x.author_id}) - # |> select([c,a,x], {c.post_id, a.id}) - # |> where([c, a], a.row_number < 3) - # |> join(:inner, [c], a in assoc(c, :author)) - # |> join(:inner, [c], a in subquery(isubquery)) - # |> group_by([c, a, x], x.author_id) - # |> distinct([c, a], a.author_id) - # |> select([c, a], {c.post_id, a.author_id}) - # |> select([c, a], {c.post_id, fragment("max(?) OVER (PARTITION BY ?)", a.id, a.id)}) - # |> select([c, a], %{post_id: c.post_id, user: fragment("max(?) OVER (PARTITION BY ?)", a.id, a.id)}) - # |> select([c, a], fragment("SELECT ROW_NUMBER() OVER (PARTITION BY ?) FROM cms_authors AS r , ", a.id)) - # |> join([c], c in subquery(sq), on: c.post_id == bq.id) - # |> having([c, a], count("*") < 10) - # |> having([c, a], a.id < 180) - # |> limit(3) - # |> order_by([p, s], desc: fragment("count(?)", s.id)) - # |> distinct([c, a], a.id) - # |> Repo.all(repo_opts) - # |> IO.inspect(label: "get fuck") - # |> bat_man() - - for id <- post_ids, do: Map.get(results, id, []) - end - - # TODO: use meta-programing to extract all query below - # -------------------- - def bat_man(data) do - # TODO refactor later - data - |> Enum.group_by(fn {x, _} -> x end) - |> Enum.map(fn {x, y} -> - {x, - Enum.reduce(y, [], fn kv, acc -> - {_, v} = kv - acc ++ [v] - end)} - end) - |> Map.new() - end - def query(Author, _args) do - # you cannot use preload with select together - # https://stackoverflow.com/questions/43010352/ecto-select-relations-from-preload - # see also - # https://github.com/elixir-ecto/ecto/issues/1145 from(a in Author, join: u in assoc(a, :user), select: u) end @@ -187,43 +93,6 @@ defmodule MastaniServer.CMS.Utils.Loader do ) end - @doc """ - get unique participators join in comments - """ - def query({"posts_comments", PostComment}, %{filter: filter, unique: true}) do - # def query({"posts_comments", PostComment}, %{unique: true}) do - PostComment - # |> QueryBuilder.members_pack(args) - |> QueryBuilder.filter_pack(filter) - |> join(:inner, [c], a in assoc(c, :author)) - |> distinct([c, a], a.id) - |> select([c, a], a) - end - - def query({"posts_comments", PostComment}, %{count: _, unique: true}) do - # TODO: not very familar with SQL, but it has to be 2 group_by to work, check later - # and the expect count should be the length of reault - PostComment - |> join(:inner, [c], a in assoc(c, :author)) - |> distinct([c, a], a.id) - |> group_by([c, a], a.id) - |> group_by([c, a], c.post_id) - |> select([c, a], count(c.id)) - end - - def query({"posts_comments", PostComment}, %{count: _}) do - PostComment - |> group_by([c], c.post_id) - |> select([c], count(c.id)) - end - - # def query({"posts_comments", PostComment}, %{filter: %{first: first}} = filter) do - def query({"posts_comments", PostComment}, %{filter: filter}) do - PostComment - # |> limit(3) - |> QueryBuilder.filter_pack(filter) - end - @doc """ handle query: 1. bacic filter of pagi,when,sort ... @@ -260,7 +129,38 @@ defmodule MastaniServer.CMS.Utils.Loader do CommunityEditor |> QueryBuilder.members_pack(args) end - # for comments replies, likes, repliesCount, likesCount... + # ------- post comments ------ + @doc """ + get unique participators join in comments + """ + def query({"posts_comments", PostComment}, %{filter: filter, unique: true}) do + PostComment + # |> QueryBuilder.filter_pack(filter) + |> join(:inner, [c], a in assoc(c, :author)) + |> distinct([c, a], a.id) + |> select([c, a], a) + end + + def query({"posts_comments", PostComment}, %{count: _, unique: true}) do + PostComment + |> join(:inner, [c], a in assoc(c, :author)) + |> distinct([c, a], a.id) + |> group_by([c, a], a.id) + |> group_by([c, a], c.post_id) + |> select([c, a], count(c.id)) + end + + def query({"posts_comments", PostComment}, %{count: _}) do + PostComment + |> group_by([c], c.post_id) + |> select([c], count(c.id)) + end + + def query({"posts_comments", PostComment}, %{filter: filter}) do + PostComment + |> QueryBuilder.filter_pack(filter) + end + def query({"posts_comments_replies", PostCommentReply}, %{count: _}) do PostCommentReply |> group_by([c], c.post_comment_id) @@ -310,6 +210,23 @@ defmodule MastaniServer.CMS.Utils.Loader do end # ---- job comments ------ + def query({"jobs_comments", JobComment}, %{filter: filter, unique: true}) do + JobComment + # |> QueryBuilder.filter_pack(filter) + |> join(:inner, [c], a in assoc(c, :author)) + |> distinct([c, a], a.id) + |> select([c, a], a) + end + + def query({"jobs_comments", JobComment}, %{count: _, unique: true}) do + JobComment + |> join(:inner, [c], a in assoc(c, :author)) + |> distinct([c, a], a.id) + |> group_by([c, a], a.id) + |> group_by([c, a], c.job_id) + |> select([c, a], count(c.id)) + end + def query({"jobs_comments", JobComment}, %{count: _}) do JobComment |> group_by([c], c.job_id) @@ -364,9 +281,26 @@ defmodule MastaniServer.CMS.Utils.Loader do JobCommentDislike |> QueryBuilder.members_pack(args) end - # ---- job ------ + # ---- job comments end------ # ---- video comments ------ + def query({"videos_comments", VideoComment}, %{filter: filter, unique: true}) do + VideoComment + # |> QueryBuilder.filter_pack(filter) + |> join(:inner, [c], a in assoc(c, :author)) + |> distinct([c, a], a.id) + |> select([c, a], a) + end + + def query({"videos_comments", VideoComment}, %{count: _, unique: true}) do + VideoComment + |> join(:inner, [c], a in assoc(c, :author)) + |> distinct([c, a], a.id) + |> group_by([c, a], a.id) + |> group_by([c, a], c.video_id) + |> select([c, a], count(c.id)) + end + def query({"videos_comments", VideoComment}, %{count: _}) do VideoComment |> group_by([c], c.video_id) @@ -426,6 +360,23 @@ defmodule MastaniServer.CMS.Utils.Loader do # ---- video ------ # --- repo comments ------ + def query({"repos_comments", RepoComment}, %{filter: filter, unique: true}) do + RepoComment + # |> QueryBuilder.filter_pack(filter) + |> join(:inner, [c], a in assoc(c, :author)) + |> distinct([c, a], a.id) + |> select([c, a], a) + end + + def query({"repos_comments", RepoComment}, %{count: _, unique: true}) do + RepoComment + |> join(:inner, [c], a in assoc(c, :author)) + |> distinct([c, a], a.id) + |> group_by([c, a], a.id) + |> group_by([c, a], c.repo_id) + |> select([c, a], count(c.id)) + end + def query({"repos_comments", RepoComment}, %{count: _}) do RepoComment |> group_by([c], c.repo_id) diff --git a/lib/mastani_server_web/middleware/cut_participators.ex b/lib/mastani_server_web/middleware/cut_participators.ex new file mode 100644 index 000000000..9713dc9c4 --- /dev/null +++ b/lib/mastani_server_web/middleware/cut_participators.ex @@ -0,0 +1,21 @@ +# --- +# cut comments participators manually +# +# --- +defmodule MastaniServerWeb.Middleware.CutParticipators do + @behaviour Absinthe.Middleware + # google: must appear in the GROUP BY clause or be used in an aggregate function + + def call(%{errors: errors} = resolution, _) when length(errors) > 0, do: resolution + + def call(%{value: value} = resolution, _) do + # IO.inspect value |> Enum.slice(0, 5), label: "hello value --> " + %{resolution | value: value |> Enum.slice(0, 5)} + end + + # def call(%{value: []} = resolution, _) do + # %{resolution | value: 0} + # end + + def call(resolution, _), do: resolution +end diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 5c87d7f27..a47fe9cac 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -37,43 +37,15 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field :comments, list_of(:comment) do arg(:filter, :members_filter) - # middleware(M.ForceLoader) middleware(M.PageSizeProof) resolve(dataloader(CMS, :comments)) end - @dec "total comments of the post" - field :comments_count, :integer do - arg(:count, :count_type, default_value: :count) - - resolve(dataloader(CMS, :comments)) - middleware(M.ConvertToInt) - end - - field :comments_participators, list_of(:user) do - arg(:filter, :members_filter) - arg(:unique, :unique_type, default_value: true) - - middleware(M.ForceLoader) - middleware(M.PageSizeProof) - resolve(dataloader(CMS, :comments)) - end - - field :comments_participators2, list_of(:user) do - arg(:filter, :members_filter) - arg(:unique, :unique_type, default_value: true) - - middleware(M.PageSizeProof) - - resolve(fn post, _args, %{context: %{loader: loader}} -> - loader - |> Dataloader.load(CMS, {:many, CMS.PostComment}, cp_users: post.id) - |> on_load(fn loader -> - {:ok, Dataloader.get(loader, CMS, {:many, CMS.PostComment}, cp_users: post.id)} - end) - end) - end + # comments_count + # comments_participators + comments_counter_fields() + @desc "totalCount of unique participator list of a the comments" field :comments_participators_count, :integer do resolve(fn post, _args, %{context: %{loader: loader}} -> loader @@ -84,21 +56,43 @@ defmodule MastaniServerWeb.Schema.CMS.Types do end) end - field :comments_participators_count_wired, :integer do - arg(:unique, :unique_type, default_value: true) - arg(:count, :count_type, default_value: :count) - - # middleware(M.ForceLoader) - resolve(dataloader(CMS, :comments)) - # middleware(M.CountLength) - end - favorite_fields(:post) star_fields(:post) timestamp_fields() end + object :job do + interface(:article) + field(:id, :id) + field(:title, :string) + field(:desc, :string) + field(:company, :string) + field(:company_logo, :string) + field(:digest, :string) + field(:location, :string) + field(:length, :integer) + field(:link_addr, :string) + field(:body, :string) + field(:views, :integer) + + field(:pin, :boolean) + field(:trash, :boolean) + + # favorite_count, viewer_did .. + + field(:author, :user, resolve: dataloader(CMS, :author)) + field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) + field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + + # comments_count + # comments_participators + comments_counter_fields() + + favorite_fields(:job) + timestamp_fields() + end + object :video do interface(:article) field(:id, :id) @@ -122,13 +116,9 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) - @dec "total comments of the video" - field :comments_count, :integer do - arg(:count, :count_type, default_value: :count) - - resolve(dataloader(CMS, :comments)) - middleware(M.ConvertToInt) - end + # comments_count + # comments_participators + comments_counter_fields() favorite_fields(:video) star_fields(:video) @@ -181,49 +171,10 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) - @dec "total comments of the repo" - field :comments_count, :integer do - arg(:count, :count_type, default_value: :count) + # comments_count + # comments_participators + comments_counter_fields() - resolve(dataloader(CMS, :comments)) - middleware(M.ConvertToInt) - end - - timestamp_fields() - end - - object :job do - interface(:article) - field(:id, :id) - field(:title, :string) - field(:desc, :string) - field(:company, :string) - field(:company_logo, :string) - field(:digest, :string) - field(:location, :string) - field(:length, :integer) - field(:link_addr, :string) - field(:body, :string) - field(:views, :integer) - - field(:pin, :boolean) - field(:trash, :boolean) - - # favorite_count, viewer_did .. - - field(:author, :user, resolve: dataloader(CMS, :author)) - field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) - field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) - - @dec "total comments of the job" - field :comments_count, :integer do - arg(:count, :count_type, default_value: :count) - - resolve(dataloader(CMS, :comments)) - middleware(M.ConvertToInt) - end - - favorite_fields(:job) timestamp_fields() end diff --git a/lib/mastani_server_web/schema/utils/helper.ex b/lib/mastani_server_web/schema/utils/helper.ex index fd24b7842..4c48738f2 100644 --- a/lib/mastani_server_web/schema/utils/helper.ex +++ b/lib/mastani_server_web/schema/utils/helper.ex @@ -213,4 +213,27 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do timestamp_fields() end end + + defmacro comments_counter_fields do + quote do + # @dec "total comments of the post" + field :comments_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(CMS, :comments)) + middleware(M.ConvertToInt) + end + + # @desc "unique participator list of a the comments" + field :comments_participators, list_of(:user) do + arg(:filter, :members_filter) + arg(:unique, :unique_type, default_value: true) + + # middleware(M.ForceLoader) + middleware(M.PageSizeProof) + resolve(dataloader(CMS, :comments)) + middleware(M.CutParticipators) + end + end + end end diff --git a/mix.exs b/mix.exs index 964860ac2..4509350f4 100644 --- a/mix.exs +++ b/mix.exs @@ -54,7 +54,7 @@ defmodule MastaniServer.Mixfile do {:phoenix, "~> 1.3.4"}, {:phoenix_pubsub, "~> 1.1.0"}, {:phoenix_ecto, "~> 3.4.0"}, - {:ecto, "~> 2.2.10"}, + {:ecto, "~> 2.2.11"}, {:postgrex, ">= 0.13.5"}, {:gettext, "~> 0.11"}, {:cowboy, "~> 1.0"}, diff --git a/mix.lock b/mix.lock index 990310302..ca9e78322 100644 --- a/mix.lock +++ b/mix.lock @@ -19,7 +19,7 @@ "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, "dialyxir": {:hex, :dialyxir, "1.0.0-rc.3", "774306f84973fc3f1e2e8743eeaa5f5d29b117f3916e5de74c075c02f1b8ef55", [:mix], [], "hexpm"}, - "ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, + "ecto": {:hex, :ecto, "2.2.11", "4bb8f11718b72ba97a2696f65d247a379e739a0ecabf6a13ad1face79844791c", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [], [], "hexpm"}, "ex_unit_notifier": {:hex, :ex_unit_notifier, "0.1.4", "36a2dcab829f506e01bf17816590680dd1474407926d43e64c1263e627c364b8", [:mix], [], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.10.0", "a4508bdd408829f38e7b2519f234b7fd5c83846099cda348efcb5291b081200c", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/mastani_server_web/query/cms/job_comment_test.exs b/test/mastani_server_web/query/cms/job_comment_test.exs index f22e31bae..b8bb39e62 100644 --- a/test/mastani_server_web/query/cms/job_comment_test.exs +++ b/test/mastani_server_web/query/cms/job_comment_test.exs @@ -13,8 +13,76 @@ defmodule MastaniServer.Test.Query.JobComment do {:ok, ~m(user_conn guest_conn job user)a} end - # TODO: user can get specific user's replies :list_replies + describe "[job dataloader comment]" do + @query """ + query($filter: PagedArticleFilter) { + pagedJobs(filter: $filter) { + entries { + id + title + commentsParticipators(filter: { first: 5 }) { + id + nickname + } + commentsCount + } + totalCount + } + } + """ + @tag :wip + test "can get comments participators of a job", ~m(user guest_conn)a do + {:ok, user2} = db_insert(:user) + + {:ok, community} = db_insert(:community) + {:ok, job} = CMS.create_content(community, :job, mock_attrs(:job), user) + variables = %{thread: "JOB", filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedJobs") + + body = "this is a test comment" + assert {:ok, _comment} = CMS.create_comment(:job, job.id, body, user) + assert {:ok, _comment} = CMS.create_comment(:job, job.id, body, user) + + assert {:ok, _comment} = CMS.create_comment(:job, job.id, body, user2) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedJobs") + + comments_count = results["entries"] |> List.first() |> Map.get("commentsCount") + + assert comments_count == 3 + end + + @tag :wip + test "can get comments participators of a job with multi user", ~m(user guest_conn)a do + body = "this is a test comment" + {:ok, community} = db_insert(:community) + {:ok, job1} = CMS.create_content(community, :job, mock_attrs(:job), user) + {:ok, job2} = CMS.create_content(community, :job, mock_attrs(:job), user) + + {:ok, users_list} = db_insert_multi(:user, 10) + {:ok, users_list2} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:job, job1.id, body, &1) + ) + + Enum.each( + users_list2, + &CMS.create_comment(:job, job2.id, body, &1) + ) + + variables = %{thread: "JOB", filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedJobs") + + assert results["entries"] |> List.first() |> Map.get("commentsParticipators") |> length == 5 + assert results["entries"] |> List.last() |> Map.get("commentsParticipators") |> length == 5 + end + end + + # TODO: user can get specific user's replies :list_replies describe "[job comment]" do @query """ query($filter: PagedArticleFilter) { diff --git a/test/mastani_server_web/query/cms/post_comment_test.exs b/test/mastani_server_web/query/cms/post_comment_test.exs index 9c3a2c7e5..13cc976a2 100644 --- a/test/mastani_server_web/query/cms/post_comment_test.exs +++ b/test/mastani_server_web/query/cms/post_comment_test.exs @@ -31,6 +31,7 @@ defmodule MastaniServer.Test.Query.PostComment do } } """ + @tag :wip test "can get comments participators of a post", ~m(user guest_conn)a do {:ok, user2} = db_insert(:user) @@ -68,6 +69,36 @@ defmodule MastaniServer.Test.Query.PostComment do assert comments_participators |> Enum.any?(&(&1["id"] == to_string(user.id))) assert comments_participators |> Enum.any?(&(&1["id"] == to_string(user2.id))) end + + @tag :wip + test "can get comments participators of a post with multi user", ~m(user guest_conn)a do + body = "this is a test comment" + {:ok, community} = db_insert(:community) + {:ok, post1} = CMS.create_content(community, :post, mock_attrs(:post), user) + {:ok, post2} = CMS.create_content(community, :post, mock_attrs(:post), user) + + {:ok, users_list} = db_insert_multi(:user, 10) + {:ok, users_list2} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:post, post1.id, body, &1) + ) + + Enum.each( + users_list2, + &CMS.create_comment(:post, post2.id, body, &1) + ) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedPosts") + + assert results["entries"] |> List.first() |> Map.get("commentsParticipators") |> length == 5 + assert results["entries"] |> List.first() |> Map.get("commentsParticipatorsCount") == 10 + + assert results["entries"] |> List.last() |> Map.get("commentsParticipators") |> length == 5 + assert results["entries"] |> List.last() |> Map.get("commentsParticipatorsCount") == 10 + end end # TODO: user can get specific user's replies :list_replies diff --git a/test/mastani_server_web/query/cms/repo_comment_test.exs b/test/mastani_server_web/query/cms/repo_comment_test.exs index 09549ae92..4e5718b10 100644 --- a/test/mastani_server_web/query/cms/repo_comment_test.exs +++ b/test/mastani_server_web/query/cms/repo_comment_test.exs @@ -13,6 +13,75 @@ defmodule MastaniServer.Test.Query.RepoComment do {:ok, ~m(user_conn guest_conn repo user)a} end + describe "[repo dataloader comment]" do + @query """ + query($filter: PagedArticleFilter) { + pagedRepos(filter: $filter) { + entries { + id + title + commentsParticipators(filter: { first: 5 }) { + id + nickname + } + commentsCount + } + totalCount + } + } + """ + @tag :wip + test "can get comments participators of a repo", ~m(user guest_conn)a do + {:ok, user2} = db_insert(:user) + + {:ok, community} = db_insert(:community) + {:ok, repo} = CMS.create_content(community, :repo, mock_attrs(:repo), user) + + variables = %{thread: "REPO", filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedRepos") + + body = "this is a test comment" + assert {:ok, _comment} = CMS.create_comment(:repo, repo.id, body, user) + assert {:ok, _comment} = CMS.create_comment(:repo, repo.id, body, user) + + assert {:ok, _comment} = CMS.create_comment(:repo, repo.id, body, user2) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedRepos") + + comments_count = results["entries"] |> List.first() |> Map.get("commentsCount") + + assert comments_count == 3 + end + + @tag :wip + test "can get comments participators of a repo with multi user", ~m(user guest_conn)a do + body = "this is a test comment" + {:ok, community} = db_insert(:community) + {:ok, repo1} = CMS.create_content(community, :repo, mock_attrs(:repo), user) + {:ok, repo2} = CMS.create_content(community, :repo, mock_attrs(:repo), user) + + {:ok, users_list} = db_insert_multi(:user, 10) + {:ok, users_list2} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:repo, repo1.id, body, &1) + ) + + Enum.each( + users_list2, + &CMS.create_comment(:repo, repo2.id, body, &1) + ) + + variables = %{thread: "REPO", filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedRepos") + + assert results["entries"] |> List.first() |> Map.get("commentsParticipators") |> length == 5 + assert results["entries"] |> List.last() |> Map.get("commentsParticipators") |> length == 5 + end + end + # TODO: user can get specific user's replies :list_replies describe "[repo comment]" do @query """ diff --git a/test/mastani_server_web/query/cms/video_comment_test.exs b/test/mastani_server_web/query/cms/video_comment_test.exs index f580a3b2a..9e89cb763 100644 --- a/test/mastani_server_web/query/cms/video_comment_test.exs +++ b/test/mastani_server_web/query/cms/video_comment_test.exs @@ -13,6 +13,75 @@ defmodule MastaniServer.Test.Query.VideoComment do {:ok, ~m(user_conn guest_conn video user)a} end + describe "[video dataloader comment]" do + @query """ + query($filter: PagedArticleFilter) { + pagedVideos(filter: $filter) { + entries { + id + title + commentsParticipators(filter: { first: 5 }) { + id + nickname + } + commentsCount + } + totalCount + } + } + """ + @tag :wip + test "can get comments participators of a video", ~m(user guest_conn)a do + {:ok, user2} = db_insert(:user) + + {:ok, community} = db_insert(:community) + {:ok, video} = CMS.create_content(community, :video, mock_attrs(:video), user) + + variables = %{thread: "VIDEO", filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedVideos") + + body = "this is a test comment" + assert {:ok, _comment} = CMS.create_comment(:video, video.id, body, user) + assert {:ok, _comment} = CMS.create_comment(:video, video.id, body, user) + + assert {:ok, _comment} = CMS.create_comment(:video, video.id, body, user2) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedVideos") + + comments_count = results["entries"] |> List.first() |> Map.get("commentsCount") + + assert comments_count == 3 + end + + @tag :wip + test "can get comments participators of a video with multi user", ~m(user guest_conn)a do + body = "this is a test comment" + {:ok, community} = db_insert(:community) + {:ok, video1} = CMS.create_content(community, :video, mock_attrs(:video), user) + {:ok, video2} = CMS.create_content(community, :video, mock_attrs(:video), user) + + {:ok, users_list} = db_insert_multi(:user, 10) + {:ok, users_list2} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:video, video1.id, body, &1) + ) + + Enum.each( + users_list2, + &CMS.create_comment(:video, video2.id, body, &1) + ) + + variables = %{thread: "VIDEO", filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedVideos") + + assert results["entries"] |> List.first() |> Map.get("commentsParticipators") |> length == 5 + assert results["entries"] |> List.last() |> Map.get("commentsParticipators") |> length == 5 + end + end + # TODO: user can get specific user's replies :list_replies describe "[video comment]" do @query """ From ca87c1207d6c6bd4d5ca02e770d91a8ae559a3a4 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 20 Oct 2018 15:00:32 +0800 Subject: [PATCH 084/129] feat(cms repo): add missing favorite fields --- .../cms/delegates/favorited_category.ex | 38 +++---- lib/mastani_server/cms/utils/loader.ex | 5 +- .../schema/cms/cms_types.ex | 29 +++--- lib/mastani_server_web/schema/utils/helper.ex | 2 +- .../query/cms/repo_test.exs | 98 ++++++++++++++++++- 5 files changed, 138 insertions(+), 34 deletions(-) diff --git a/lib/mastani_server/cms/delegates/favorited_category.ex b/lib/mastani_server/cms/delegates/favorited_category.ex index 10a490b6b..9b681011d 100644 --- a/lib/mastani_server/cms/delegates/favorited_category.ex +++ b/lib/mastani_server/cms/delegates/favorited_category.ex @@ -9,29 +9,33 @@ defmodule MastaniServer.CMS.Delegate.FavoritedContents do alias MastaniServer.CMS def favorited_category(:post, id, %User{id: user_id}) do - case ORM.find_by(CMS.PostFavorite, post_id: id, user_id: user_id) do - {:ok, post_favorite} -> - {:ok, post_favorite.category_id} - - _ -> - {:ok, nil} - end + CMS.PostFavorite + |> ORM.find_by(post_id: id, user_id: user_id) + |> handle_reault end def favorited_category(:job, id, %User{id: user_id}) do - case ORM.find_by(CMS.JobFavorite, job_id: id, user_id: user_id) do - {:ok, job_favorite} -> - {:ok, job_favorite.category_id} - - _ -> - {:ok, nil} - end + CMS.JobFavorite + |> ORM.find_by(job_id: id, user_id: user_id) + |> handle_reault end def favorited_category(:video, id, %User{id: user_id}) do - case ORM.find_by(CMS.VideoFavorite, video_id: id, user_id: user_id) do - {:ok, video_favorite} -> - {:ok, video_favorite.category_id} + CMS.VideoFavorite + |> ORM.find_by(video_id: id, user_id: user_id) + |> handle_reault + end + + def favorited_category(:repo, id, %User{id: user_id}) do + CMS.RepoFavorite + |> ORM.find_by(repo_id: id, user_id: user_id) + |> handle_reault + end + + defp handle_reault(result) do + case result do + {:ok, content} -> + {:ok, content.category_id} _ -> {:ok, nil} diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index 9e816761c..4f8c67ece 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -36,6 +36,7 @@ defmodule MastaniServer.CMS.Utils.Loader do VideoCommentDislike, VideoCommentLike, # repo + RepoFavorite, RepoComment, RepoCommentReply, RepoCommentLike, @@ -111,7 +112,9 @@ defmodule MastaniServer.CMS.Utils.Loader do JobFavorite |> QueryBuilder.members_pack(args) end - # def query({"jobs_stars", JobStar}, args), do: JobStar |> QueryBuilder.members_pack(args) + def query({"repos_favorites", RepoFavorite}, args) do + RepoFavorite |> QueryBuilder.members_pack(args) + end def query({"videos_favorites", VideoFavorite}, args) do VideoFavorite |> QueryBuilder.members_pack(args) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index a47fe9cac..9f10c09f1 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -56,6 +56,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do end) end + # fields for: favorite count, favorited_users, viewer_did_favorite.. favorite_fields(:post) star_fields(:post) @@ -79,8 +80,6 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:pin, :boolean) field(:trash, :boolean) - # favorite_count, viewer_did .. - field(:author, :user, resolve: dataloader(CMS, :author)) field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) @@ -89,6 +88,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # comments_participators comments_counter_fields() + # fields for: favorite count, favorited_users, viewer_did_favorite.. favorite_fields(:job) timestamp_fields() end @@ -120,22 +120,12 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # comments_participators comments_counter_fields() + # fields for: favorite count, favorited_users, viewer_did_favorite.. favorite_fields(:video) star_fields(:video) timestamp_fields() end - object :repo_contributor do - field(:avatar, :string) - field(:html_url, :string) - field(:nickname, :string) - end - - object :repo_lang do - field(:name, :string) - field(:color, :string) - end - object :repo do # interface(:article) field(:id, :id) @@ -174,10 +164,23 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # comments_count # comments_participators comments_counter_fields() + # fields for: favorite count, favorited_users, viewer_did_favorite.. + favorite_fields(:repo) timestamp_fields() end + object :repo_contributor do + field(:avatar, :string) + field(:html_url, :string) + field(:nickname, :string) + end + + object :repo_lang do + field(:name, :string) + field(:color, :string) + end + object :github_contributor do field(:avatar, :string) field(:bio, :string) diff --git a/lib/mastani_server_web/schema/utils/helper.ex b/lib/mastani_server_web/schema/utils/helper.ex index 4c48738f2..30987ca65 100644 --- a/lib/mastani_server_web/schema/utils/helper.ex +++ b/lib/mastani_server_web/schema/utils/helper.ex @@ -53,7 +53,7 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do alias MastaniServerWeb.Resolvers, as: R alias MastaniServerWeb.Middleware, as: M - # fields for: favorite count, users, viewer_did_favorite.. + # fields for: favorite count, favorited_users, viewer_did_favorite.. defmacro favorite_fields(thread) do quote do @doc "if viewer has favroted of this #{unquote(thread)}" diff --git a/test/mastani_server_web/query/cms/repo_test.exs b/test/mastani_server_web/query/cms/repo_test.exs index c2aaa3948..cd50b67ab 100644 --- a/test/mastani_server_web/query/cms/repo_test.exs +++ b/test/mastani_server_web/query/cms/repo_test.exs @@ -15,16 +15,49 @@ defmodule MastaniServer.Test.Query.Repo do repo(id: $id) { id title + readme } } """ - test "basic graphql query on repo by user", ~m(guest_conn repo)a do + @tag :wip + test "basic graphql query on repo with logined user", ~m(user_conn repo)a do + variables = %{id: repo.id} + results = user_conn |> query_result(@query, variables, "repo") + + assert results["id"] == to_string(repo.id) + assert is_valid_kv?(results, "title", :string) + assert is_valid_kv?(results, "readme", :string) + assert length(Map.keys(results)) == 3 + end + + @tag :wip + test "basic graphql query on repo with stranger(unloged user)", ~m(guest_conn repo)a do variables = %{id: repo.id} results = guest_conn |> query_result(@query, variables, "repo") assert results["id"] == to_string(repo.id) assert is_valid_kv?(results, "title", :string) - assert length(Map.keys(results)) == 2 + assert is_valid_kv?(results, "readme", :string) + end + + @query """ + query($id: ID!) { + repo(id: $id) { + id + favoritedUsers { + nickname + id + } + } + } + """ + @tag :wip + test "repo have favoritedUsers query field", ~m(user_conn repo)a do + variables = %{id: repo.id} + results = user_conn |> query_result(@query, variables, "repo") + + assert results["id"] == to_string(repo.id) + assert is_valid_kv?(results, "favoritedUsers", :list) end @query """ @@ -34,6 +67,7 @@ defmodule MastaniServer.Test.Query.Repo do } } """ + @tag :wip test "views should +1 after query the repo", ~m(user_conn repo)a do variables = %{id: repo.id} views_1 = user_conn |> query_result(@query, variables, "repo") |> Map.get("views") @@ -42,4 +76,64 @@ defmodule MastaniServer.Test.Query.Repo do views_2 = user_conn |> query_result(@query, variables, "repo") |> Map.get("views") assert views_2 == views_1 + 1 end + + @query """ + query($id: ID!) { + repo(id: $id) { + id + title + viewerHasFavorited + } + } + """ + @tag :wip + test "logged user can query viewerHasFavorited field", ~m(user_conn repo)a do + variables = %{id: repo.id} + + assert user_conn + |> query_result(@query, variables, "repo") + |> has_boolen_value?("viewerHasFavorited") + end + + @tag :wip + test "unlogged user can not query viewerHasFavorited field", ~m(guest_conn repo)a do + variables = %{id: repo.id} + + assert guest_conn |> query_get_error?(@query, variables) + end + + alias MastaniServer.Accounts + + @query """ + query($id: ID!) { + repo(id: $id) { + id + favoritedCategoryId + } + } + """ + @tag :wip + test "login user can get nil repo favorited category id", ~m(repo)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + variables = %{id: repo.id} + result = user_conn |> query_result(@query, variables, "repo") + assert result["favoritedCategoryId"] == nil + end + + @tag :wip + test "login user can get repo favorited category id after favorited", ~m(repo)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + test_category = "test category" + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, _favorite_category} = Accounts.set_favorites(user, :repo, repo.id, category.id) + + variables = %{id: repo.id} + result = user_conn |> query_result(@query, variables, "repo") + + assert result["favoritedCategoryId"] == to_string(category.id) + end end From 8529c04fa56ba83b4c13ddf2d89238c246a46da9 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 20 Oct 2018 15:02:25 +0800 Subject: [PATCH 085/129] chore(clean up): wip tag --- test/mastani_server_web/query/cms/job_comment_test.exs | 2 -- test/mastani_server_web/query/cms/post_comment_test.exs | 2 -- test/mastani_server_web/query/cms/repo_comment_test.exs | 2 -- test/mastani_server_web/query/cms/repo_test.exs | 8 -------- test/mastani_server_web/query/cms/video_comment_test.exs | 2 -- 5 files changed, 16 deletions(-) diff --git a/test/mastani_server_web/query/cms/job_comment_test.exs b/test/mastani_server_web/query/cms/job_comment_test.exs index b8bb39e62..87d17f9a3 100644 --- a/test/mastani_server_web/query/cms/job_comment_test.exs +++ b/test/mastani_server_web/query/cms/job_comment_test.exs @@ -30,7 +30,6 @@ defmodule MastaniServer.Test.Query.JobComment do } } """ - @tag :wip test "can get comments participators of a job", ~m(user guest_conn)a do {:ok, user2} = db_insert(:user) @@ -54,7 +53,6 @@ defmodule MastaniServer.Test.Query.JobComment do assert comments_count == 3 end - @tag :wip test "can get comments participators of a job with multi user", ~m(user guest_conn)a do body = "this is a test comment" {:ok, community} = db_insert(:community) diff --git a/test/mastani_server_web/query/cms/post_comment_test.exs b/test/mastani_server_web/query/cms/post_comment_test.exs index 13cc976a2..69d1e27cd 100644 --- a/test/mastani_server_web/query/cms/post_comment_test.exs +++ b/test/mastani_server_web/query/cms/post_comment_test.exs @@ -31,7 +31,6 @@ defmodule MastaniServer.Test.Query.PostComment do } } """ - @tag :wip test "can get comments participators of a post", ~m(user guest_conn)a do {:ok, user2} = db_insert(:user) @@ -70,7 +69,6 @@ defmodule MastaniServer.Test.Query.PostComment do assert comments_participators |> Enum.any?(&(&1["id"] == to_string(user2.id))) end - @tag :wip test "can get comments participators of a post with multi user", ~m(user guest_conn)a do body = "this is a test comment" {:ok, community} = db_insert(:community) diff --git a/test/mastani_server_web/query/cms/repo_comment_test.exs b/test/mastani_server_web/query/cms/repo_comment_test.exs index 4e5718b10..763aa6af6 100644 --- a/test/mastani_server_web/query/cms/repo_comment_test.exs +++ b/test/mastani_server_web/query/cms/repo_comment_test.exs @@ -30,7 +30,6 @@ defmodule MastaniServer.Test.Query.RepoComment do } } """ - @tag :wip test "can get comments participators of a repo", ~m(user guest_conn)a do {:ok, user2} = db_insert(:user) @@ -54,7 +53,6 @@ defmodule MastaniServer.Test.Query.RepoComment do assert comments_count == 3 end - @tag :wip test "can get comments participators of a repo with multi user", ~m(user guest_conn)a do body = "this is a test comment" {:ok, community} = db_insert(:community) diff --git a/test/mastani_server_web/query/cms/repo_test.exs b/test/mastani_server_web/query/cms/repo_test.exs index cd50b67ab..630e07569 100644 --- a/test/mastani_server_web/query/cms/repo_test.exs +++ b/test/mastani_server_web/query/cms/repo_test.exs @@ -19,7 +19,6 @@ defmodule MastaniServer.Test.Query.Repo do } } """ - @tag :wip test "basic graphql query on repo with logined user", ~m(user_conn repo)a do variables = %{id: repo.id} results = user_conn |> query_result(@query, variables, "repo") @@ -30,7 +29,6 @@ defmodule MastaniServer.Test.Query.Repo do assert length(Map.keys(results)) == 3 end - @tag :wip test "basic graphql query on repo with stranger(unloged user)", ~m(guest_conn repo)a do variables = %{id: repo.id} results = guest_conn |> query_result(@query, variables, "repo") @@ -51,7 +49,6 @@ defmodule MastaniServer.Test.Query.Repo do } } """ - @tag :wip test "repo have favoritedUsers query field", ~m(user_conn repo)a do variables = %{id: repo.id} results = user_conn |> query_result(@query, variables, "repo") @@ -67,7 +64,6 @@ defmodule MastaniServer.Test.Query.Repo do } } """ - @tag :wip test "views should +1 after query the repo", ~m(user_conn repo)a do variables = %{id: repo.id} views_1 = user_conn |> query_result(@query, variables, "repo") |> Map.get("views") @@ -86,7 +82,6 @@ defmodule MastaniServer.Test.Query.Repo do } } """ - @tag :wip test "logged user can query viewerHasFavorited field", ~m(user_conn repo)a do variables = %{id: repo.id} @@ -95,7 +90,6 @@ defmodule MastaniServer.Test.Query.Repo do |> has_boolen_value?("viewerHasFavorited") end - @tag :wip test "unlogged user can not query viewerHasFavorited field", ~m(guest_conn repo)a do variables = %{id: repo.id} @@ -112,7 +106,6 @@ defmodule MastaniServer.Test.Query.Repo do } } """ - @tag :wip test "login user can get nil repo favorited category id", ~m(repo)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -122,7 +115,6 @@ defmodule MastaniServer.Test.Query.Repo do assert result["favoritedCategoryId"] == nil end - @tag :wip test "login user can get repo favorited category id after favorited", ~m(repo)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) diff --git a/test/mastani_server_web/query/cms/video_comment_test.exs b/test/mastani_server_web/query/cms/video_comment_test.exs index 9e89cb763..89d113e88 100644 --- a/test/mastani_server_web/query/cms/video_comment_test.exs +++ b/test/mastani_server_web/query/cms/video_comment_test.exs @@ -30,7 +30,6 @@ defmodule MastaniServer.Test.Query.VideoComment do } } """ - @tag :wip test "can get comments participators of a video", ~m(user guest_conn)a do {:ok, user2} = db_insert(:user) @@ -54,7 +53,6 @@ defmodule MastaniServer.Test.Query.VideoComment do assert comments_count == 3 end - @tag :wip test "can get comments participators of a video with multi user", ~m(user guest_conn)a do body = "this is a test comment" {:ok, community} = db_insert(:community) From 89448dae4eb4786c4cd1d117d9dcf2f0fe34537d Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 20 Oct 2018 15:42:51 +0800 Subject: [PATCH 086/129] chore(docs): comments dataloader --- lib/mastani_server/cms/utils/loader.ex | 3 +++ .../middleware/cut_participators.ex | 24 ++++++++++--------- .../middleware/force_loader.ex | 9 +++---- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index 4f8c67ece..6dd675a50 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -136,6 +136,9 @@ defmodule MastaniServer.CMS.Utils.Loader do @doc """ get unique participators join in comments """ + # NOTE: this is NOT the right solution + # should use WINDOW function + # see https://github.com/coderplanets/coderplanets_server/issues/16 def query({"posts_comments", PostComment}, %{filter: filter, unique: true}) do PostComment # |> QueryBuilder.filter_pack(filter) diff --git a/lib/mastani_server_web/middleware/cut_participators.ex b/lib/mastani_server_web/middleware/cut_participators.ex index 9713dc9c4..bdf3082f4 100644 --- a/lib/mastani_server_web/middleware/cut_participators.ex +++ b/lib/mastani_server_web/middleware/cut_participators.ex @@ -1,21 +1,23 @@ -# --- -# cut comments participators manually -# -# --- defmodule MastaniServerWeb.Middleware.CutParticipators do + @moduledoc """ + # cut comments participators manually by count + # this tem solution may have performace issue when the content's comments + # has too much participators + # + # NOTE: this is NOT the right solution + # should use WINDOW function + # see https://github.com/coderplanets/coderplanets_server/issues/16 + # + """ + @behaviour Absinthe.Middleware - # google: must appear in the GROUP BY clause or be used in an aggregate function + @count 5 def call(%{errors: errors} = resolution, _) when length(errors) > 0, do: resolution def call(%{value: value} = resolution, _) do - # IO.inspect value |> Enum.slice(0, 5), label: "hello value --> " - %{resolution | value: value |> Enum.slice(0, 5)} + %{resolution | value: value |> Enum.slice(0, @count)} end - # def call(%{value: []} = resolution, _) do - # %{resolution | value: 0} - # end - def call(resolution, _), do: resolution end diff --git a/lib/mastani_server_web/middleware/force_loader.ex b/lib/mastani_server_web/middleware/force_loader.ex index 85475086b..9e1fb6486 100644 --- a/lib/mastani_server_web/middleware/force_loader.ex +++ b/lib/mastani_server_web/middleware/force_loader.ex @@ -1,8 +1,9 @@ -# this is a tmp solution for load related-users like situations -# it turn dataloader into nomal N+1 resolver -# NOTE: it should be replaced using "Select-Top-N-By-Group" solution - defmodule MastaniServerWeb.Middleware.ForceLoader do + @moduledoc """ + # this is a tmp solution for load related-users like situations + # it turn dataloader into nomal N+1 resolver + # NOTE: it should be replaced using "Select-Top-N-By-Group" solution + """ @behaviour Absinthe.Middleware def call(%{source: %{id: id}} = resolution, _) do From 8bd0761c2851aa265f5702ed7a2b14fe8f75b996 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 20 Oct 2018 16:47:45 +0800 Subject: [PATCH 087/129] refactor(comments): add paged participators --- lib/mastani_server/cms/cms.ex | 4 +- .../cms/delegates/comment_curd.ex | 21 +++++++ .../resolvers/cms_resolver.ex | 8 +++ .../schema/cms/cms_queries.ex | 10 ++++ .../schema/cms/cms_types.ex | 10 ++-- lib/mastani_server_web/schema/utils/helper.ex | 12 +++- .../query/cms/job_comment_test.exs | 58 +++++++++++++++++++ .../query/cms/post_comment_test.exs | 58 +++++++++++++++++++ .../query/cms/repo_comment_test.exs | 58 +++++++++++++++++++ .../query/cms/video_comment_test.exs | 58 +++++++++++++++++++ 10 files changed, 290 insertions(+), 7 deletions(-) diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index c8624f3ca..bc9dd4e16 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -81,9 +81,11 @@ defmodule MastaniServer.CMS do defdelegate unset_community(community, thread, content_id), to: ArticleOperation # Comment CURD + defdelegate list_comments(thread, content_id, filters), to: CommentCURD + defdelegate list_comments_participators(thread, content_id, filters), to: CommentCURD + defdelegate create_comment(thread, content_id, body, user), to: CommentCURD defdelegate delete_comment(thread, content_id), to: CommentCURD - defdelegate list_comments(thread, content_id, filters), to: CommentCURD defdelegate list_replies(thread, comment, user), to: CommentCURD defdelegate reply_comment(thread, comment, body, user), to: CommentCURD diff --git a/lib/mastani_server/cms/delegates/comment_curd.ex b/lib/mastani_server/cms/delegates/comment_curd.ex index 79dfecdda..c111ae998 100644 --- a/lib/mastani_server/cms/delegates/comment_curd.ex +++ b/lib/mastani_server/cms/delegates/comment_curd.ex @@ -73,6 +73,9 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do {:error, [message: "update follor fails", code: ecode(:delete_fails)]} end + @doc """ + list paged comments + """ def list_comments(thread, content_id, %{page: page, size: size} = filters) do with {:ok, action} <- match_action(thread, :comment) do dynamic = dynamic_comment_where(thread, content_id) @@ -85,6 +88,24 @@ defmodule MastaniServer.CMS.Delegate.CommentCURD do end end + @doc """ + list paged comments participators + """ + def list_comments_participators(thread, content_id, %{page: page, size: size} = filters) do + with {:ok, action} <- match_action(thread, :comment) do + dynamic = dynamic_comment_where(thread, content_id) + + action.reactor + |> where(^dynamic) + |> QueryBuilder.filter_pack(filters) + |> join(:inner, [c], a in assoc(c, :author)) + |> distinct([c, a], a.id) + |> select([c, a], a) + |> ORM.paginater(~m(page size)a) + |> done() + end + end + def list_replies(thread, comment_id, %Accounts.User{id: user_id}) do with {:ok, action} <- match_action(thread, :comment) do action.reactor diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 4026bc6fd..334b819c1 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -223,6 +223,14 @@ defmodule MastaniServerWeb.Resolvers.CMS do CMS.list_comments(thread, id, filter) end + def paged_comments_participators(_root, ~m(id thread filter)a, _info) do + CMS.list_comments_participators(thread, id, filter) + end + + def paged_comments_participators(root, ~m(thread)a, _info) do + CMS.list_comments_participators(thread, root.id, %{page: 1, size: 20}) + end + def create_comment(_root, ~m(thread id body)a, %{context: %{cur_user: user}}) do CMS.create_comment(thread, id, body, user) end diff --git a/lib/mastani_server_web/schema/cms/cms_queries.ex b/lib/mastani_server_web/schema/cms/cms_queries.ex index 67c87ba58..179cc02b5 100644 --- a/lib/mastani_server_web/schema/cms/cms_queries.ex +++ b/lib/mastani_server_web/schema/cms/cms_queries.ex @@ -181,6 +181,16 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do resolve(&R.CMS.paged_comments/3) end + @desc "get paged comments participators" + field :paged_comments_participators, :paged_users do + arg(:id, non_null(:id)) + arg(:thread, :cms_thread, default_value: :post) + arg(:filter, :paged_filter) + + middleware(M.PageSizeProof) + resolve(&R.CMS.paged_comments_participators/3) + end + # comments # TODO: remove field :comments, :paged_comments do diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 9f10c09f1..2c49ac2d9 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -42,8 +42,8 @@ defmodule MastaniServerWeb.Schema.CMS.Types do end # comments_count - # comments_participators - comments_counter_fields() + # comments_participators / paged + comments_counter_fields(:post) @desc "totalCount of unique participator list of a the comments" field :comments_participators_count, :integer do @@ -86,7 +86,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # comments_count # comments_participators - comments_counter_fields() + comments_counter_fields(:job) # fields for: favorite count, favorited_users, viewer_did_favorite.. favorite_fields(:job) @@ -118,7 +118,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # comments_count # comments_participators - comments_counter_fields() + comments_counter_fields(:video) # fields for: favorite count, favorited_users, viewer_did_favorite.. favorite_fields(:video) @@ -163,7 +163,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # comments_count # comments_participators - comments_counter_fields() + comments_counter_fields(:repo) # fields for: favorite count, favorited_users, viewer_did_favorite.. favorite_fields(:repo) diff --git a/lib/mastani_server_web/schema/utils/helper.ex b/lib/mastani_server_web/schema/utils/helper.ex index 30987ca65..1c3c255ce 100644 --- a/lib/mastani_server_web/schema/utils/helper.ex +++ b/lib/mastani_server_web/schema/utils/helper.ex @@ -214,7 +214,7 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do end end - defmacro comments_counter_fields do + defmacro comments_counter_fields(thread) do quote do # @dec "total comments of the post" field :comments_count, :integer do @@ -234,6 +234,16 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do resolve(dataloader(CMS, :comments)) middleware(M.CutParticipators) end + + field(:paged_comments_participators, :paged_users) do + arg( + :thread, + unquote(String.to_atom("#{to_string(thread)}_thread")), + default_value: unquote(thread) + ) + + resolve(&R.CMS.paged_comments_participators/3) + end end end end diff --git a/test/mastani_server_web/query/cms/job_comment_test.exs b/test/mastani_server_web/query/cms/job_comment_test.exs index 87d17f9a3..6641d6d9a 100644 --- a/test/mastani_server_web/query/cms/job_comment_test.exs +++ b/test/mastani_server_web/query/cms/job_comment_test.exs @@ -24,6 +24,12 @@ defmodule MastaniServer.Test.Query.JobComment do id nickname } + pagedCommentsParticipators { + entries { + id + } + totalCount + } commentsCount } totalCount @@ -78,6 +84,58 @@ defmodule MastaniServer.Test.Query.JobComment do assert results["entries"] |> List.first() |> Map.get("commentsParticipators") |> length == 5 assert results["entries"] |> List.last() |> Map.get("commentsParticipators") |> length == 5 end + + test "can get paged commetns participators of a job", ~m(user guest_conn)a do + body = "this is a test comment" + + {:ok, community} = db_insert(:community) + {:ok, job} = CMS.create_content(community, :job, mock_attrs(:job), user) + {:ok, users_list} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:job, job.id, body, &1) + ) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedJobs") + participators = results["entries"] |> List.first() |> Map.get("pagedCommentsParticipators") + + assert participators["totalCount"] == 10 + end + end + + @query """ + query($id: ID!, $thread: CmsThread, $filter: PagedFilter!) { + pagedCommentsParticipators(id: $id, thread: $thread, filter: $filter) { + entries { + id + nickname + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + test "can get job's paged commetns participators", ~m(user guest_conn)a do + body = "this is a test comment" + + {:ok, community} = db_insert(:community) + {:ok, job} = CMS.create_content(community, :job, mock_attrs(:job), user) + {:ok, users_list} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:job, job.id, body, &1) + ) + + variables = %{id: job.id, thread: "JOB", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "pagedCommentsParticipators") + assert results |> is_valid_pagination?() + + assert results["totalCount"] == 10 end # TODO: user can get specific user's replies :list_replies diff --git a/test/mastani_server_web/query/cms/post_comment_test.exs b/test/mastani_server_web/query/cms/post_comment_test.exs index 69d1e27cd..2f53dc13a 100644 --- a/test/mastani_server_web/query/cms/post_comment_test.exs +++ b/test/mastani_server_web/query/cms/post_comment_test.exs @@ -25,6 +25,12 @@ defmodule MastaniServer.Test.Query.PostComment do id nickname } + pagedCommentsParticipators { + entries { + id + } + totalCount + } commentsCount } totalCount @@ -97,6 +103,58 @@ defmodule MastaniServer.Test.Query.PostComment do assert results["entries"] |> List.last() |> Map.get("commentsParticipators") |> length == 5 assert results["entries"] |> List.last() |> Map.get("commentsParticipatorsCount") == 10 end + + test "can get paged commetns participators of a post", ~m(user guest_conn)a do + body = "this is a test comment" + + {:ok, community} = db_insert(:community) + {:ok, post} = CMS.create_content(community, :post, mock_attrs(:post), user) + {:ok, users_list} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:post, post.id, body, &1) + ) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedPosts") + participators = results["entries"] |> List.first() |> Map.get("pagedCommentsParticipators") + + assert participators["totalCount"] == 10 + end + end + + @query """ + query($id: ID!, $thread: CmsThread, $filter: PagedFilter!) { + pagedCommentsParticipators(id: $id, thread: $thread, filter: $filter) { + entries { + id + nickname + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + test "can get post's paged commetns participators", ~m(user guest_conn)a do + body = "this is a test comment" + + {:ok, community} = db_insert(:community) + {:ok, post} = CMS.create_content(community, :post, mock_attrs(:post), user) + {:ok, users_list} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:post, post.id, body, &1) + ) + + variables = %{id: post.id, thread: "POST", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "pagedCommentsParticipators") + assert results |> is_valid_pagination?() + + assert results["totalCount"] == 10 end # TODO: user can get specific user's replies :list_replies diff --git a/test/mastani_server_web/query/cms/repo_comment_test.exs b/test/mastani_server_web/query/cms/repo_comment_test.exs index 763aa6af6..22ceb3236 100644 --- a/test/mastani_server_web/query/cms/repo_comment_test.exs +++ b/test/mastani_server_web/query/cms/repo_comment_test.exs @@ -24,6 +24,12 @@ defmodule MastaniServer.Test.Query.RepoComment do id nickname } + pagedCommentsParticipators { + entries { + id + } + totalCount + } commentsCount } totalCount @@ -78,6 +84,58 @@ defmodule MastaniServer.Test.Query.RepoComment do assert results["entries"] |> List.first() |> Map.get("commentsParticipators") |> length == 5 assert results["entries"] |> List.last() |> Map.get("commentsParticipators") |> length == 5 end + + test "can get paged commetns participators of a repo", ~m(user guest_conn)a do + body = "this is a test comment" + + {:ok, community} = db_insert(:community) + {:ok, repo} = CMS.create_content(community, :repo, mock_attrs(:repo), user) + {:ok, users_list} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:repo, repo.id, body, &1) + ) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedRepos") + participators = results["entries"] |> List.first() |> Map.get("pagedCommentsParticipators") + + assert participators["totalCount"] == 10 + end + end + + @query """ + query($id: ID!, $thread: CmsThread, $filter: PagedFilter!) { + pagedCommentsParticipators(id: $id, thread: $thread, filter: $filter) { + entries { + id + nickname + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + test "can get repo's paged commetns participators", ~m(user guest_conn)a do + body = "this is a test comment" + + {:ok, community} = db_insert(:community) + {:ok, repo} = CMS.create_content(community, :repo, mock_attrs(:repo), user) + {:ok, users_list} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:repo, repo.id, body, &1) + ) + + variables = %{id: repo.id, thread: "REPO", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "pagedCommentsParticipators") + assert results |> is_valid_pagination?() + + assert results["totalCount"] == 10 end # TODO: user can get specific user's replies :list_replies diff --git a/test/mastani_server_web/query/cms/video_comment_test.exs b/test/mastani_server_web/query/cms/video_comment_test.exs index 89d113e88..9bb0f546c 100644 --- a/test/mastani_server_web/query/cms/video_comment_test.exs +++ b/test/mastani_server_web/query/cms/video_comment_test.exs @@ -24,6 +24,12 @@ defmodule MastaniServer.Test.Query.VideoComment do id nickname } + pagedCommentsParticipators { + entries { + id + } + totalCount + } commentsCount } totalCount @@ -78,6 +84,58 @@ defmodule MastaniServer.Test.Query.VideoComment do assert results["entries"] |> List.first() |> Map.get("commentsParticipators") |> length == 5 assert results["entries"] |> List.last() |> Map.get("commentsParticipators") |> length == 5 end + + test "can get paged commetns participators of a video", ~m(user guest_conn)a do + body = "this is a test comment" + + {:ok, community} = db_insert(:community) + {:ok, video} = CMS.create_content(community, :video, mock_attrs(:video), user) + {:ok, users_list} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:video, video.id, body, &1) + ) + + variables = %{filter: %{community: community.raw}} + results = guest_conn |> query_result(@query, variables, "pagedVideos") + participators = results["entries"] |> List.first() |> Map.get("pagedCommentsParticipators") + + assert participators["totalCount"] == 10 + end + end + + @query """ + query($id: ID!, $thread: CmsThread, $filter: PagedFilter!) { + pagedCommentsParticipators(id: $id, thread: $thread, filter: $filter) { + entries { + id + nickname + } + totalPages + totalCount + pageSize + pageNumber + } + } + """ + test "can get video's paged commetns participators", ~m(user guest_conn)a do + body = "this is a test comment" + + {:ok, community} = db_insert(:community) + {:ok, video} = CMS.create_content(community, :video, mock_attrs(:video), user) + {:ok, users_list} = db_insert_multi(:user, 10) + + Enum.each( + users_list, + &CMS.create_comment(:video, video.id, body, &1) + ) + + variables = %{id: video.id, thread: "VIDEO", filter: %{page: 1, size: 20}} + results = guest_conn |> query_result(@query, variables, "pagedCommentsParticipators") + assert results |> is_valid_pagination?() + + assert results["totalCount"] == 10 end # TODO: user can get specific user's replies :list_replies From 0e319b4e6ae6cc882aa7e5c915b3ae4377944db7 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 22 Oct 2018 09:04:15 +0800 Subject: [PATCH 088/129] test: missing fields --- test/mastani_server_web/query/cms/post_test.exs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/mastani_server_web/query/cms/post_test.exs b/test/mastani_server_web/query/cms/post_test.exs index 39a225f40..63d25f0aa 100644 --- a/test/mastani_server_web/query/cms/post_test.exs +++ b/test/mastani_server_web/query/cms/post_test.exs @@ -127,6 +127,12 @@ defmodule MastaniServer.Test.Query.Post do post(id: $id) { id favoritedCategoryId + pagedCommentsParticipators { + entries { + id + } + totalCount + } } } """ From 68020fd117f3d79a82f7bb2366582fd98271b6e1 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 22 Oct 2018 11:01:20 +0800 Subject: [PATCH 089/129] fix(follow choas): follow / undofollow done right --- lib/mastani_server/accounts/accounts.ex | 1 - lib/mastani_server/accounts/delegates/fans.ex | 18 ++++++------------ lib/mastani_server/accounts/utils/loader.ex | 4 ---- .../resolvers/accounts_resolver.ex | 2 -- .../schema/account/account_types.ex | 11 ++--------- test/mastani_server/accounts/fans_test.exs | 8 ++++---- .../mutation/accounts/fans_test.exs | 8 +++++--- .../query/accounts/achievement_test.exs | 2 +- .../query/accounts/fans_test.exs | 3 ++- 9 files changed, 20 insertions(+), 37 deletions(-) diff --git a/lib/mastani_server/accounts/accounts.ex b/lib/mastani_server/accounts/accounts.ex index 944b52613..0b9114f64 100644 --- a/lib/mastani_server/accounts/accounts.ex +++ b/lib/mastani_server/accounts/accounts.ex @@ -43,7 +43,6 @@ defmodule MastaniServer.Accounts do defdelegate undo_follow(user, follower), to: Fans defdelegate fetch_followers(user, filter), to: Fans defdelegate fetch_followings(user, filter), to: Fans - defdelegate count_followers(user), to: Fans # reacted contents defdelegate reacted_contents(thread, react, filter, user), to: ReactedContents diff --git a/lib/mastani_server/accounts/delegates/fans.ex b/lib/mastani_server/accounts/delegates/fans.ex index 96220998f..3e5e6c60e 100644 --- a/lib/mastani_server/accounts/delegates/fans.ex +++ b/lib/mastani_server/accounts/delegates/fans.ex @@ -24,7 +24,8 @@ defmodule MastaniServer.Accounts.Delegate.Fans do Multi.new() |> Multi.insert( :create_follower, - UserFollower.changeset(%UserFollower{}, ~m(user_id follower_id)a) + # UserFollower.changeset(%UserFollower{}, ~m(user_id follower_id)a) + UserFollower.changeset(%UserFollower{}, %{user_id: follower_id, follower_id: user_id}) ) |> Multi.insert( :create_following, @@ -46,7 +47,7 @@ defmodule MastaniServer.Accounts.Delegate.Fans do @spec follow_result({:ok, map()}) :: SpecType.done() defp follow_result({:ok, %{create_follower: user_follower}}) do - User |> ORM.find(user_follower.follower_id) + User |> ORM.find(user_follower.user_id) end defp follow_result({:error, :create_follower, _result, _steps}) do @@ -70,7 +71,7 @@ defmodule MastaniServer.Accounts.Delegate.Fans do {:ok, _follow_user} <- ORM.find(User, follower_id) do Multi.new() |> Multi.run(:delete_follower, fn _ -> - ORM.findby_delete(UserFollower, ~m(user_id follower_id)a) + ORM.findby_delete(UserFollower, %{user_id: follower_id, follower_id: user_id}) end) |> Multi.run(:delete_following, fn _ -> ORM.findby_delete(UserFollowing, %{user_id: user_id, following_id: follower_id}) @@ -105,21 +106,14 @@ defmodule MastaniServer.Accounts.Delegate.Fans do {:error, [message: "follow acieve fails", code: ecode(:react_fails)]} end - def count_followers(%User{id: user_id}) do - UserFollower - |> where([uf], uf.follower_id == ^user_id) - |> ORM.count() - |> done() - end - @doc """ get paged followers of a user """ @spec fetch_followers(User.t(), map()) :: {:ok, map()} | {:error, String.t()} def fetch_followers(%User{id: user_id}, filter) do UserFollower - |> where([uf], uf.follower_id == ^user_id) - |> join(:inner, [uf], u in assoc(uf, :user)) + |> where([uf], uf.user_id == ^user_id) + |> join(:inner, [uf], u in assoc(uf, :follower)) |> load_fans(filter) end diff --git a/lib/mastani_server/accounts/utils/loader.ex b/lib/mastani_server/accounts/utils/loader.ex index 781d490f5..336c4badf 100644 --- a/lib/mastani_server/accounts/utils/loader.ex +++ b/lib/mastani_server/accounts/utils/loader.ex @@ -26,10 +26,6 @@ defmodule MastaniServer.Accounts.Utils.Loader do # TODO: fix later, this is not working def query({"users_followers", UserFollower}, %{count: _}) do - # UserFollower - # |> group_by([f], f.user_id) - # |> select([f], count(f.id)) - UserFollower |> group_by([f], f.user_id) |> select([f], count(f.follower_id)) diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index ea0a4df44..478262440 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -76,8 +76,6 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.undo_follow(cur_user, %User{id: user_id}) end - def count_followers(root, _args, _info), do: Accounts.count_followers(%User{id: root.id}) - def paged_followers(_root, ~m(user_id filter)a, _info) do Accounts.fetch_followers(%User{id: user_id}, filter) end diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index b08a07a2f..59e40a9ec 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -87,19 +87,12 @@ defmodule MastaniServerWeb.Schema.Account.Types do resolve(&R.Accounts.editable_communities/3) end - # NOTE: dataloader not work at this case - # field :followers_count, :integer do - # arg(:count, :count_type, default_value: :count) - - # resolve(dataloader(Accounts, :followers)) - # middleware(M.ConvertToInt) - # end - @doc "get follower users count" field :followers_count, :integer do arg(:count, :count_type, default_value: :count) - resolve(&R.Accounts.count_followers/3) + resolve(dataloader(Accounts, :followers)) + middleware(M.ConvertToInt) end @doc "get following users count" diff --git a/test/mastani_server/accounts/fans_test.exs b/test/mastani_server/accounts/fans_test.exs index 0dadf59d5..e9bbe67d0 100644 --- a/test/mastani_server/accounts/fans_test.exs +++ b/test/mastani_server/accounts/fans_test.exs @@ -17,9 +17,9 @@ defmodule MastaniServer.Test.Accounts.Fans do {:ok, user2} = db_insert(:user) {:ok, _followeer} = user |> Accounts.follow(user2) - {:ok, found} = User |> ORM.find(user.id, preload: :followers) + {:ok, found} = User |> ORM.find(user2.id, preload: :followers) - assert found |> Map.get(:followers) |> Enum.any?(&(&1.user_id == user.id)) + assert found |> Map.get(:followers) |> Enum.any?(&(&1.follower_id == user.id)) assert found |> Map.get(:followers) |> length == 1 end @@ -52,12 +52,12 @@ defmodule MastaniServer.Test.Accounts.Fans do {:ok, user2} = db_insert(:user) {:ok, _followeer} = user |> Accounts.follow(user2) - {:ok, found} = User |> ORM.find(user.id, preload: :followers) + {:ok, found} = User |> ORM.find(user2.id, preload: :followers) assert found |> Map.get(:followers) |> length == 1 {:ok, _followeer} = user |> Accounts.undo_follow(user2) - {:ok, found} = User |> ORM.find(user.id, preload: :followers) + {:ok, found} = User |> ORM.find(user2.id, preload: :followers) assert found |> Map.get(:followers) |> length == 0 end end diff --git a/test/mastani_server_web/mutation/accounts/fans_test.exs b/test/mastani_server_web/mutation/accounts/fans_test.exs index 3caae4d4f..310754c44 100644 --- a/test/mastani_server_web/mutation/accounts/fans_test.exs +++ b/test/mastani_server_web/mutation/accounts/fans_test.exs @@ -20,6 +20,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.Fans do mutation($userId: ID!) { follow(userId: $userId) { id + viewerHasFollowed } } """ @@ -30,6 +31,7 @@ defmodule MastaniServer.Test.Mutation.Accounts.Fans do followed = user_conn |> mutation_result(@query, variables, "follow") assert followed["id"] == to_string(user2.id) + assert followed["viewerHasFollowed"] == true end test "login user follow other user twice fails", ~m(user_conn)a do @@ -70,16 +72,16 @@ defmodule MastaniServer.Test.Mutation.Accounts.Fans do {:ok, user2} = db_insert(:user) {:ok, _followeer} = user |> Accounts.follow(user2) - {:ok, found} = User |> ORM.find(user.id, preload: :followers) + {:ok, found} = User |> ORM.find(user2.id, preload: :followers) assert found |> Map.get(:followers) |> length == 1 variables = %{userId: user2.id} user_conn |> mutation_result(@query, variables, "undoFollow") - {:ok, found} = User |> ORM.find(user.id, preload: :followers) + {:ok, found} = User |> ORM.find(user2.id, preload: :followers) assert found |> Map.get(:followers) |> length == 0 - {:ok, found} = User |> ORM.find(user.id, preload: :followings) + {:ok, found} = User |> ORM.find(user2.id, preload: :followings) assert found |> Map.get(:followings) |> length == 0 end end diff --git a/test/mastani_server_web/query/accounts/achievement_test.exs b/test/mastani_server_web/query/accounts/achievement_test.exs index 7b2a08ada..436ffcfe8 100644 --- a/test/mastani_server_web/query/accounts/achievement_test.exs +++ b/test/mastani_server_web/query/accounts/achievement_test.exs @@ -123,7 +123,7 @@ defmodule MastaniServer.Test.Query.Account.Achievement do assert results["achievement"] |> Map.get("reputation") == 2 * @follow_weight end - test "minus user's achievement after user get cancle followed", ~m(guest_conn user)a do + test "minus user's achievement after user get undo followed", ~m(guest_conn user)a do total_count = 10 {:ok, users} = db_insert_multi(:user, total_count) diff --git a/test/mastani_server_web/query/accounts/fans_test.exs b/test/mastani_server_web/query/accounts/fans_test.exs index be7b77915..102707f11 100644 --- a/test/mastani_server_web/query/accounts/fans_test.exs +++ b/test/mastani_server_web/query/accounts/fans_test.exs @@ -148,9 +148,10 @@ defmodule MastaniServer.Test.Query.Account.Fans do resolts = user_conn |> query_result(@query, variables, "user") assert resolts |> Map.get("viewerHasFollowed") == false - {:ok, _} = user2 |> Accounts.follow(user) + {:ok, _} = user |> Accounts.follow(user2) variables = %{id: user2.id} resolts = user_conn |> query_result(@query, variables, "user") + assert resolts |> Map.get("viewerHasFollowed") == true end end From 00d7783ab75c3e74f0cea36494841d7d3d97db8a Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 22 Oct 2018 11:04:02 +0800 Subject: [PATCH 090/129] chore(clean up): test file warnings --- test/mastani_server_web/query/cms/job_comment_test.exs | 2 +- test/mastani_server_web/query/cms/repo_comment_test.exs | 2 +- test/mastani_server_web/query/cms/video_comment_test.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/mastani_server_web/query/cms/job_comment_test.exs b/test/mastani_server_web/query/cms/job_comment_test.exs index 6641d6d9a..b763b1369 100644 --- a/test/mastani_server_web/query/cms/job_comment_test.exs +++ b/test/mastani_server_web/query/cms/job_comment_test.exs @@ -43,7 +43,7 @@ defmodule MastaniServer.Test.Query.JobComment do {:ok, job} = CMS.create_content(community, :job, mock_attrs(:job), user) variables = %{thread: "JOB", filter: %{community: community.raw}} - results = guest_conn |> query_result(@query, variables, "pagedJobs") + guest_conn |> query_result(@query, variables, "pagedJobs") body = "this is a test comment" assert {:ok, _comment} = CMS.create_comment(:job, job.id, body, user) diff --git a/test/mastani_server_web/query/cms/repo_comment_test.exs b/test/mastani_server_web/query/cms/repo_comment_test.exs index 22ceb3236..c0ed76112 100644 --- a/test/mastani_server_web/query/cms/repo_comment_test.exs +++ b/test/mastani_server_web/query/cms/repo_comment_test.exs @@ -43,7 +43,7 @@ defmodule MastaniServer.Test.Query.RepoComment do {:ok, repo} = CMS.create_content(community, :repo, mock_attrs(:repo), user) variables = %{thread: "REPO", filter: %{community: community.raw}} - results = guest_conn |> query_result(@query, variables, "pagedRepos") + guest_conn |> query_result(@query, variables, "pagedRepos") body = "this is a test comment" assert {:ok, _comment} = CMS.create_comment(:repo, repo.id, body, user) diff --git a/test/mastani_server_web/query/cms/video_comment_test.exs b/test/mastani_server_web/query/cms/video_comment_test.exs index 9bb0f546c..3ded7bd21 100644 --- a/test/mastani_server_web/query/cms/video_comment_test.exs +++ b/test/mastani_server_web/query/cms/video_comment_test.exs @@ -43,7 +43,7 @@ defmodule MastaniServer.Test.Query.VideoComment do {:ok, video} = CMS.create_content(community, :video, mock_attrs(:video), user) variables = %{thread: "VIDEO", filter: %{community: community.raw}} - results = guest_conn |> query_result(@query, variables, "pagedVideos") + guest_conn |> query_result(@query, variables, "pagedVideos") body = "this is a test comment" assert {:ok, _comment} = CMS.create_comment(:video, video.id, body, user) From ac8c5034d7985bfb346c9b53c20cb2581664b9ed Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 22 Oct 2018 13:00:08 +0800 Subject: [PATCH 091/129] test(favoreted repos): add missing test for f-repos --- lib/mastani_server/accounts/user.ex | 1 + lib/mastani_server/accounts/utils/loader.ex | 4 + .../schema/account/account_queries.ex | 13 +- .../schema/account/account_types.ex | 17 +++ .../query/accounts/favorited_repos_test.exs | 124 ++++++++++++++++++ 5 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 test/mastani_server_web/query/accounts/favorited_repos_test.exs diff --git a/lib/mastani_server/accounts/user.ex b/lib/mastani_server/accounts/user.ex index 04af5c274..542bc8c9f 100644 --- a/lib/mastani_server/accounts/user.ex +++ b/lib/mastani_server/accounts/user.ex @@ -59,6 +59,7 @@ defmodule MastaniServer.Accounts.User do has_many(:favorited_posts, {"posts_favorites", CMS.PostFavorite}) has_many(:favorited_jobs, {"jobs_favorites", CMS.JobFavorite}) has_many(:favorited_videos, {"videos_favorites", CMS.VideoFavorite}) + has_many(:favorited_repos, {"repos_favorites", CMS.RepoFavorite}) has_many(:favorite_categories, {"favorite_categories", FavoriteCategory}) diff --git a/lib/mastani_server/accounts/utils/loader.ex b/lib/mastani_server/accounts/utils/loader.ex index 336c4badf..817e2cc83 100644 --- a/lib/mastani_server/accounts/utils/loader.ex +++ b/lib/mastani_server/accounts/utils/loader.ex @@ -67,6 +67,10 @@ defmodule MastaniServer.Accounts.Utils.Loader do CMS.VideoFavorite |> count_contents end + def query({"repos_favorites", CMS.RepoFavorite}, %{count: _}) do + CMS.RepoFavorite |> count_contents + end + def query(queryable, _args), do: queryable defp count_contents(queryable) do diff --git a/lib/mastani_server_web/schema/account/account_queries.ex b/lib/mastani_server_web/schema/account/account_queries.ex index 1ad44b791..0cdae22ea 100644 --- a/lib/mastani_server_web/schema/account/account_queries.ex +++ b/lib/mastani_server_web/schema/account/account_queries.ex @@ -120,7 +120,7 @@ defmodule MastaniServerWeb.Schema.Account.Queries do resolve(&R.Accounts.favorited_contents/3) end - @desc "get favorited jobs" + @desc "get favorited videos" field :favorited_videos, :paged_videos do arg(:user_id, non_null(:id)) arg(:filter, non_null(:paged_filter)) @@ -131,6 +131,17 @@ defmodule MastaniServerWeb.Schema.Account.Queries do resolve(&R.Accounts.favorited_contents/3) end + @desc "get favorited repos" + field :favorited_repos, :paged_repos do + arg(:user_id, non_null(:id)) + arg(:filter, non_null(:paged_filter)) + arg(:category_id, :id) + arg(:thread, :repo_thread, default_value: :repo) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.favorited_contents/3) + end + @desc "get paged published posts" field :published_posts, :paged_posts do arg(:user_id, non_null(:id)) diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index 59e40a9ec..479bd8d58 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -167,6 +167,15 @@ defmodule MastaniServerWeb.Schema.Account.Types do resolve(&R.Accounts.favorited_contents/3) end + @doc "paged favorited repos" + field :favorited_repos, :paged_repos do + arg(:filter, non_null(:paged_filter)) + arg(:thread, :repo_thread, default_value: :repo) + + middleware(M.PageSizeProof) + resolve(&R.Accounts.favorited_contents/3) + end + @doc "total count of stared posts count" field :stared_posts_count, :integer do arg(:count, :count_type, default_value: :count) @@ -215,6 +224,14 @@ defmodule MastaniServerWeb.Schema.Account.Types do middleware(M.ConvertToInt) end + @doc "total count of favorited videos count" + field :favorited_repos_count, :integer do + arg(:count, :count_type, default_value: :count) + + resolve(dataloader(Accounts, :favorited_repos)) + middleware(M.ConvertToInt) + end + field :contributes, :contribute_map do resolve(&R.Statistics.list_contributes/3) end diff --git a/test/mastani_server_web/query/accounts/favorited_repos_test.exs b/test/mastani_server_web/query/accounts/favorited_repos_test.exs new file mode 100644 index 000000000..37bd31ffa --- /dev/null +++ b/test/mastani_server_web/query/accounts/favorited_repos_test.exs @@ -0,0 +1,124 @@ +defmodule MastaniServer.Test.Query.Accounts.FavritedRepos do + use MastaniServer.TestTools + + alias MastaniServer.CMS + + @total_count 20 + + setup do + {:ok, user} = db_insert(:user) + {:ok, repos} = db_insert_multi(:repo, @total_count) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(guest_conn user_conn user repos)a} + end + + describe "[accounts favorited repos]" do + @query """ + query($filter: PagedFilter!) { + account { + id + favoritedRepos(filter: $filter) { + entries { + id + } + totalCount + } + favoritedReposCount + } + } + """ + test "login user can get it's own favoritedRepos", ~m(user_conn user repos)a do + Enum.each(repos, fn repo -> + {:ok, _} = CMS.reaction(:repo, :favorite, repo.id, user) + end) + + random_id = repos |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string + + variables = %{filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "account") + assert results["favoritedRepos"] |> Map.get("totalCount") == @total_count + assert results["favoritedReposCount"] == @total_count + + assert results["favoritedRepos"] + |> Map.get("entries") + |> Enum.any?(&(&1["id"] == random_id)) + end + + @query """ + query($userId: ID!, $categoryId: ID,$filter: PagedFilter!) { + favoritedRepos(userId: $userId, categoryId: $categoryId, filter: $filter) { + entries { + id + } + totalCount + } + } + """ + test "other user can get other user's paged favoritedRepos", + ~m(user_conn guest_conn repos)a do + {:ok, user} = db_insert(:user) + + Enum.each(repos, fn repo -> + {:ok, _} = CMS.reaction(:repo, :favorite, repo.id, user) + end) + + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "favoritedRepos") + results2 = guest_conn |> query_result(@query, variables, "favoritedRepos") + + assert results["totalCount"] == @total_count + assert results2["totalCount"] == @total_count + end + + test "login user can get self paged favoritedRepos", ~m(user_conn user repos)a do + Enum.each(repos, fn repo -> + {:ok, _} = CMS.reaction(:repo, :favorite, repo.id, user) + end) + + variables = %{userId: user.id, filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "favoritedRepos") + + assert results["totalCount"] == @total_count + end + + alias MastaniServer.Accounts + + test "can get paged favoritedRepos on a spec category", ~m(user_conn guest_conn repos)a do + {:ok, user} = db_insert(:user) + + Enum.each(repos, fn repo -> + {:ok, _} = CMS.reaction(:repo, :favorite, repo.id, user) + end) + + repo1 = Enum.at(repos, 0) + repo2 = Enum.at(repos, 1) + repo3 = Enum.at(repos, 2) + repo4 = Enum.at(repos, 4) + + test_category = "test category" + test_category2 = "test category2" + + {:ok, category} = Accounts.create_favorite_category(user, %{title: test_category}) + {:ok, category2} = Accounts.create_favorite_category(user, %{title: test_category2}) + + {:ok, _favorites_category} = Accounts.set_favorites(user, :repo, repo1.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :repo, repo2.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :repo, repo3.id, category.id) + {:ok, _favorites_category} = Accounts.set_favorites(user, :repo, repo4.id, category2.id) + + variables = %{userId: user.id, categoryId: category.id, filter: %{page: 1, size: 20}} + results = user_conn |> query_result(@query, variables, "favoritedRepos") + results2 = guest_conn |> query_result(@query, variables, "favoritedRepos") + + assert results["totalCount"] == 3 + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(repo1.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(repo2.id))) + assert results["entries"] |> Enum.any?(&(&1["id"] == to_string(repo3.id))) + + assert results == results2 + end + end +end From 14f14bc0c3cafba4377a275eb11082dd19c8f39a Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 22 Oct 2018 14:23:24 +0800 Subject: [PATCH 092/129] refactor(cms repos): rename last_fetch_time -> last_sync --- lib/mastani_server/cms/repo.ex | 4 ++-- lib/mastani_server_web/schema/cms/cms_types.ex | 2 ++ .../migrations/20181022060833_rename_repo_last_fetch.exs | 7 +++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 priv/repo/migrations/20181022060833_rename_repo_last_fetch.exs diff --git a/lib/mastani_server/cms/repo.ex b/lib/mastani_server/cms/repo.ex index 4889c9929..270e08739 100644 --- a/lib/mastani_server/cms/repo.ex +++ b/lib/mastani_server/cms/repo.ex @@ -17,7 +17,7 @@ defmodule MastaniServer.CMS.Repo do } @required_fields ~w(title owner_name owner_url repo_url desc readme star_count issues_count prs_count fork_count watch_count)a - @optional_fields ~w(last_fetch_time homepage_url release_tag license) + @optional_fields ~w(last_sync homepage_url release_tag license) @type t :: %Repo{} schema "cms_repos" do @@ -50,7 +50,7 @@ defmodule MastaniServer.CMS.Repo do field(:pin, :boolean, default_value: false) field(:trash, :boolean, default_value: false) - field(:last_fetch_time, :utc_datetime) + field(:last_sync, :utc_datetime) has_many(:comments, {"repos_comments", RepoComment}) has_many(:favorites, {"repos_favorites", RepoFavorite}) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 2c49ac2d9..bed24b733 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -158,6 +158,8 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # field(:pin, :boolean) # field(:trash, :boolean) + field(:last_sync, :datetime) + field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) diff --git a/priv/repo/migrations/20181022060833_rename_repo_last_fetch.exs b/priv/repo/migrations/20181022060833_rename_repo_last_fetch.exs new file mode 100644 index 000000000..70ed066ba --- /dev/null +++ b/priv/repo/migrations/20181022060833_rename_repo_last_fetch.exs @@ -0,0 +1,7 @@ +defmodule MastaniServer.Repo.Migrations.RenameRepoLastFetch do + use Ecto.Migration + + def change do + rename(table(:cms_repos), :last_fetch_time, to: :last_sync) + end +end From 3e8990c5d831b5ca2c4a170fb4e68f3995c118e4 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Mon, 22 Oct 2018 21:42:09 +0800 Subject: [PATCH 093/129] fix(repo favrote): totalCount query --- lib/helper/query_builder.ex | 6 ++++++ test/mastani_server_web/query/cms/repo_test.exs | 1 + 2 files changed, 7 insertions(+) diff --git a/lib/helper/query_builder.ex b/lib/helper/query_builder.ex index b8c0a1d34..6a3d1cfe0 100644 --- a/lib/helper/query_builder.ex +++ b/lib/helper/query_builder.ex @@ -39,6 +39,12 @@ defmodule Helper.QueryBuilder do |> select([f], count(f.id)) end + def members_pack(queryable, %{count: _, type: :repo}) do + queryable + |> group_by([f], f.repo_id) + |> select([f], count(f.id)) + end + def members_pack(queryable, %{count: _, type: :community}) do queryable |> group_by([f], f.community_id) diff --git a/test/mastani_server_web/query/cms/repo_test.exs b/test/mastani_server_web/query/cms/repo_test.exs index 630e07569..c855d75e6 100644 --- a/test/mastani_server_web/query/cms/repo_test.exs +++ b/test/mastani_server_web/query/cms/repo_test.exs @@ -79,6 +79,7 @@ defmodule MastaniServer.Test.Query.Repo do id title viewerHasFavorited + favoritedCount } } """ From 87b9ba55a75090605ac55ee7b1d3124d5b9e3e4c Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 23 Oct 2018 09:21:59 +0800 Subject: [PATCH 094/129] feat(user): add views field --- lib/mastani_server/accounts/user.ex | 2 ++ .../resolvers/accounts_resolver.ex | 7 +++---- .../schema/account/account_types.ex | 2 ++ .../20181023004300_add_views_to_user.exs | 9 +++++++++ .../query/accounts/account_test.exs | 14 +++++++++++++- 5 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 priv/repo/migrations/20181023004300_add_views_to_user.exs diff --git a/lib/mastani_server/accounts/user.ex b/lib/mastani_server/accounts/user.ex index 542bc8c9f..d0d430ba7 100644 --- a/lib/mastani_server/accounts/user.ex +++ b/lib/mastani_server/accounts/user.ex @@ -36,6 +36,8 @@ defmodule MastaniServer.Accounts.User do field(:from_github, :boolean) field(:geo_city, :string) + field(:views, :integer, default: 0) + sscial_fields() embeds_many(:education_backgrounds, EducationBackground) diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 478262440..6e30e0adf 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -9,7 +9,7 @@ defmodule MastaniServerWeb.Resolvers.Accounts do alias Accounts.{MentionMail, NotificationMail, SysNotificationMail, User} - def user(_root, %{id: id}, _info), do: User |> ORM.find(id) + def user(_root, %{id: id}, _info), do: User |> ORM.read(id, inc: :views) def users(_root, ~m(filter)a, _info), do: User |> ORM.find_all(filter) def session_state(_root, _args, %{context: %{cur_user: cur_user}}), @@ -17,9 +17,8 @@ defmodule MastaniServerWeb.Resolvers.Accounts do def session_state(_root, _args, _info), do: {:ok, %{is_valid: false}} - def account(_root, _args, %{context: %{cur_user: cur_user}}) do - User |> ORM.find(cur_user.id) - end + def account(_root, _args, %{context: %{cur_user: cur_user}}), + do: User |> ORM.read(cur_user.id, inc: :views) def update_profile(_root, args, %{context: %{cur_user: cur_user}}) do profile = diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index 479bd8d58..bb4528c8a 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -34,6 +34,8 @@ defmodule MastaniServerWeb.Schema.Account.Types do field(:location, :string) field(:geo_city, :string) + field(:views, :integer) + sscial_fields() field(:inserted_at, :datetime) diff --git a/priv/repo/migrations/20181023004300_add_views_to_user.exs b/priv/repo/migrations/20181023004300_add_views_to_user.exs new file mode 100644 index 000000000..eba61c166 --- /dev/null +++ b/priv/repo/migrations/20181023004300_add_views_to_user.exs @@ -0,0 +1,9 @@ +defmodule MastaniServer.Repo.Migrations.AddViewsToUser do + use Ecto.Migration + + def change do + alter table(:users) do + add(:views, :integer, default: 0) + end + end +end diff --git a/test/mastani_server_web/query/accounts/account_test.exs b/test/mastani_server_web/query/accounts/account_test.exs index cf7c4aa51..a8a7079a8 100644 --- a/test/mastani_server_web/query/accounts/account_test.exs +++ b/test/mastani_server_web/query/accounts/account_test.exs @@ -2,7 +2,9 @@ defmodule MastaniServer.Test.Query.Account.Basic do use MastaniServer.TestTools import Helper.Utils, only: [get_config: 2] - alias MastaniServer.CMS + + alias Helper.ORM + alias MastaniServer.{Accounts, CMS} @default_subscribed_communities get_config(:general, :default_subscribed_communities) @@ -54,6 +56,7 @@ defmodule MastaniServer.Test.Query.Account.Basic do id nickname bio + views cmsPassport cmsPassportString educationBackgrounds { @@ -78,6 +81,15 @@ defmodule MastaniServer.Test.Query.Account.Basic do assert results["cmsPassport"] == nil end + test "user's views +1 after visit", ~m(guest_conn user)a do + {:ok, target_user} = ORM.find(Accounts.User, user.id) + assert target_user.views == 0 + + variables = %{id: user.id} + results = guest_conn |> query_result(@query, variables, "user") + assert results["views"] == 1 + end + test "login newbie user can get own empty cms_passport", ~m(user)a do user_conn = simu_conn(:user, user) variables = %{id: user.id} From 327c3ef40de5a2f2b5e6e12fabe942cbe69285b6 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 23 Oct 2018 20:11:13 +0800 Subject: [PATCH 095/129] feat(posts): add copy_right fields --- lib/mastani_server/cms/post.ex | 3 ++- lib/mastani_server_web/resolvers/cms_resolver.ex | 5 +++-- lib/mastani_server_web/schema/cms/cms_types.ex | 1 + lib/mastani_server_web/schema/cms/mutations/post.ex | 1 + .../20181023112702_add_copyright_to_posts.exs | 9 +++++++++ test/mastani_server_web/mutation/cms/post_test.exs | 10 +++++++--- 6 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 priv/repo/migrations/20181023112702_add_copyright_to_posts.exs diff --git a/lib/mastani_server/cms/post.ex b/lib/mastani_server/cms/post.ex index c07bbf640..90fdc3513 100644 --- a/lib/mastani_server/cms/post.ex +++ b/lib/mastani_server/cms/post.ex @@ -16,7 +16,7 @@ defmodule MastaniServer.CMS.Post do } @required_fields ~w(title body digest length)a - @optional_fields ~w(link_addr)a + @optional_fields ~w(link_addr copy_right)a @type t :: %Post{} schema "cms_posts" do @@ -24,6 +24,7 @@ defmodule MastaniServer.CMS.Post do field(:title, :string) field(:digest, :string) field(:link_addr, :string) + field(:copy_right, :string) field(:length, :integer) field(:views, :integer, default: 0) diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 334b819c1..01f6bffe1 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -49,8 +49,9 @@ defmodule MastaniServerWeb.Resolvers.CMS do CMS.create_content(%Community{id: community_id}, thread, args, user) end - def update_content(_root, %{passport_source: content} = args, _info), - do: ORM.update(content, args) + def update_content(_root, %{passport_source: content} = args, _info) do + ORM.update(content, args) + end def delete_content(_root, %{passport_source: content}, _info), do: ORM.delete(content) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index bed24b733..5f8692b84 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -24,6 +24,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:digest, :string) field(:length, :integer) field(:link_addr, :string) + field(:copy_right, :string) field(:body, :string) field(:views, :integer) # TODO: remove diff --git a/lib/mastani_server_web/schema/cms/mutations/post.ex b/lib/mastani_server_web/schema/cms/mutations/post.ex index 221515b18..ad3877dd7 100644 --- a/lib/mastani_server_web/schema/cms/mutations/post.ex +++ b/lib/mastani_server_web/schema/cms/mutations/post.ex @@ -90,6 +90,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Post do arg(:title, :string) arg(:body, :string) arg(:digest, :string) + arg(:copy_right, :string) middleware(M.Authorize, :login) middleware(M.PassportLoader, source: :post) diff --git a/priv/repo/migrations/20181023112702_add_copyright_to_posts.exs b/priv/repo/migrations/20181023112702_add_copyright_to_posts.exs new file mode 100644 index 000000000..4a4574e05 --- /dev/null +++ b/priv/repo/migrations/20181023112702_add_copyright_to_posts.exs @@ -0,0 +1,9 @@ +defmodule MastaniServer.Repo.Migrations.AddCopyrightToPosts do + use Ecto.Migration + + def change do + alter table(:cms_posts) do + add(:copy_right, :string, default: "original") + end + end +end diff --git a/test/mastani_server_web/mutation/cms/post_test.exs b/test/mastani_server_web/mutation/cms/post_test.exs index 14ed7cae7..5f3f514e0 100644 --- a/test/mastani_server_web/mutation/cms/post_test.exs +++ b/test/mastani_server_web/mutation/cms/post_test.exs @@ -111,11 +111,12 @@ defmodule MastaniServer.Test.Mutation.Post do end @query """ - mutation($id: ID!, $title: String, $body: String){ - updatePost(id: $id, title: $title, body: $body) { + mutation($id: ID!, $title: String, $body: String, $copyRight: String){ + updatePost(id: $id, title: $title, body: $body, copyRight: $copyRight) { id title body + copyRight } } """ @@ -131,19 +132,22 @@ defmodule MastaniServer.Test.Mutation.Post do assert guest_conn |> mutation_get_error?(@query, variables, ecode(:account_login)) end + @tag :wip test "post can be update by owner", ~m(owner_conn post)a do unique_num = System.unique_integer([:positive, :monotonic]) variables = %{ id: post.id, title: "updated title #{unique_num}", - body: "updated body #{unique_num}" + body: "updated body #{unique_num}", + copyRight: "translate" } updated_post = owner_conn |> mutation_result(@query, variables, "updatePost") assert updated_post["title"] == variables.title assert updated_post["body"] == variables.body + assert updated_post["copyRight"] == variables.copyRight end test "login user with auth passport update a post", ~m(post)a do From 90056bf418c4ea39cd7c2212c2f8d3900fc66d0d Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 23 Oct 2018 22:26:01 +0800 Subject: [PATCH 096/129] fix: missing copy_right field --- lib/mastani_server_web/schema/cms/mutations/post.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mastani_server_web/schema/cms/mutations/post.ex b/lib/mastani_server_web/schema/cms/mutations/post.ex index ad3877dd7..7d101ac7d 100644 --- a/lib/mastani_server_web/schema/cms/mutations/post.ex +++ b/lib/mastani_server_web/schema/cms/mutations/post.ex @@ -12,6 +12,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Post do arg(:digest, non_null(:string)) arg(:length, non_null(:integer)) arg(:link_addr, :string) + arg(:copy_right, :string) arg(:community_id, non_null(:id)) arg(:thread, :cms_thread, default_value: :post) arg(:tags, list_of(:ids)) From b64f9f5391a6943a7eec137d8747c2b6a9e9bd4b Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 23 Oct 2018 23:35:43 +0800 Subject: [PATCH 097/129] feat(viewers): add viewer_has_viewed to posts --- lib/mastani_server/cms/cms.ex | 1 + .../cms/delegates/article_curd.ex | 17 +++ lib/mastani_server/cms/post.ex | 2 + lib/mastani_server/cms/post_viewer.ex | 27 +++++ lib/mastani_server/cms/utils/loader.ex | 5 + lib/mastani_server/cms/utils/matcher.ex | 5 +- .../resolvers/cms_resolver.ex | 6 + .../schema/cms/cms_types.ex | 9 ++ .../20181023142819_create_posts_viewers.exs | 14 +++ .../mutation/cms/post_test.exs | 1 - .../query/cms/post_test.exs | 16 --- .../query/cms/post_viewer_test.exs | 105 ++++++++++++++++++ 12 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 lib/mastani_server/cms/post_viewer.ex create mode 100644 priv/repo/migrations/20181023142819_create_posts_viewers.exs create mode 100644 test/mastani_server_web/query/cms/post_viewer_test.exs diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index bc9dd4e16..28741b0ab 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -60,6 +60,7 @@ defmodule MastaniServer.CMS do defdelegate unsubscribe_community(community, user, remote_ip), to: CommunityOperation # ArticleCURD + defdelegate read_content(thread, id, user), to: ArticleCURD defdelegate paged_contents(queryable, filter), to: ArticleCURD defdelegate create_content(community, thread, attrs, user), to: ArticleCURD defdelegate reaction_users(thread, react, id, filters), to: ArticleCURD diff --git a/lib/mastani_server/cms/delegates/article_curd.ex b/lib/mastani_server/cms/delegates/article_curd.ex index 22e093c2a..a143b2dbd 100644 --- a/lib/mastani_server/cms/delegates/article_curd.ex +++ b/lib/mastani_server/cms/delegates/article_curd.ex @@ -19,6 +19,23 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do alias CMS.{Author, Community, Tag} alias Ecto.Multi + @doc """ + login user read cms content by add views count and viewer record + """ + def read_content(thread, id, %User{id: user_id}) do + condition = %{user_id: user_id} |> Map.merge(content_id(thread, id)) + + with {:ok, action} <- match_action(thread, :self), + {:ok, _viewer} <- action.viewer |> ORM.findby_or_insert(condition, condition) do + action.target |> ORM.read(id, inc: :views) + end + end + + defp content_id(:post, id), do: %{post_id: id} + defp content_id(:job, id), do: %{job_id: id} + defp content_id(:repo, id), do: %{repo_id: id} + defp content_id(:video, id), do: %{video_id: id} + @doc """ get paged post / job ... """ diff --git a/lib/mastani_server/cms/post.ex b/lib/mastani_server/cms/post.ex index 90fdc3513..a47d6eab3 100644 --- a/lib/mastani_server/cms/post.ex +++ b/lib/mastani_server/cms/post.ex @@ -12,6 +12,7 @@ defmodule MastaniServer.CMS.Post do PostCommunityFlag, PostFavorite, PostStar, + PostViewer, Tag } @@ -42,6 +43,7 @@ defmodule MastaniServer.CMS.Post do has_many(:comments, {"posts_comments", PostComment}) has_many(:favorites, {"posts_favorites", PostFavorite}) has_many(:stars, {"posts_stars", PostStar}) + has_many(:viewers, {"posts_viewers", PostViewer}) # The keys are inflected from the schema names! # see https://hexdocs.pm/ecto/Ecto.Schema.html many_to_many( diff --git a/lib/mastani_server/cms/post_viewer.ex b/lib/mastani_server/cms/post_viewer.ex new file mode 100644 index 000000000..c7fd396d2 --- /dev/null +++ b/lib/mastani_server/cms/post_viewer.ex @@ -0,0 +1,27 @@ +defmodule MastaniServer.CMS.PostViewer do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.Post + + @required_fields ~w(post_id user_id)a + + @type t :: %PostViewer{} + schema "posts_viewers" do + belongs_to(:post, Post, foreign_key: :post_id) + belongs_to(:user, Accounts.User, foreign_key: :user_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%PostViewer{} = post_viewer, attrs) do + post_viewer + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> unique_constraint(:user_id, name: :posts_viewers_post_id_user_id_index) + end +end diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index 6dd675a50..f0c6c3f8b 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -14,6 +14,7 @@ defmodule MastaniServer.CMS.Utils.Loader do CommunityThread, # POST Post, + PostViewer, PostComment, PostCommentDislike, PostCommentLike, @@ -94,6 +95,10 @@ defmodule MastaniServer.CMS.Utils.Loader do ) end + def query({"posts_viewers", PostViewer}, %{cur_user: cur_user}) do + PostViewer |> where([pv], pv.user_id == ^cur_user.id) + end + @doc """ handle query: 1. bacic filter of pagi,when,sort ... diff --git a/lib/mastani_server/cms/utils/matcher.ex b/lib/mastani_server/cms/utils/matcher.ex index d35a73fbd..7f03be2df 100644 --- a/lib/mastani_server/cms/utils/matcher.ex +++ b/lib/mastani_server/cms/utils/matcher.ex @@ -11,6 +11,8 @@ defmodule MastaniServer.CMS.Utils.Matcher do Video, Repo, Job, + # viewer + PostViewer, # reactions PostFavorite, JobFavorite, @@ -60,7 +62,8 @@ defmodule MastaniServer.CMS.Utils.Matcher do ######################################### ## posts ... ######################################### - def match_action(:post, :self), do: {:ok, %{target: Post, reactor: Post, preload: :author}} + def match_action(:post, :self), + do: {:ok, %{target: Post, reactor: Post, preload: :author, viewer: PostViewer}} def match_action(:post, :favorite), do: {:ok, %{target: Post, reactor: PostFavorite, preload: :user, preload_right: :post}} diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 01f6bffe1..75a9ecea0 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -32,7 +32,13 @@ defmodule MastaniServerWeb.Resolvers.CMS do # ####################### # community thread (post, job) # ####################### + # def post(_root, %{id: id}, _info), do: Post |> ORM.read(id, inc: :views) + def post(_root, %{id: id}, %{context: %{cur_user: user}}) do + CMS.read_content(:post, id, user) + end + def post(_root, %{id: id}, _info), do: Post |> ORM.read(id, inc: :views) + def video(_root, %{id: id}, _info), do: Video |> ORM.read(id, inc: :views) def repo(_root, %{id: id}, _info), do: Repo |> ORM.read(id, inc: :views) def job(_root, %{id: id}, _info), do: Job |> ORM.read(id, inc: :views) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 5f8692b84..0e7fec75d 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -57,6 +57,15 @@ defmodule MastaniServerWeb.Schema.CMS.Types do end) end + @desc "if user has viewed this post" + field :viewer_has_viewed, :boolean do + middleware(M.Authorize, :login) + middleware(M.PutCurrentUser) + + resolve(dataloader(CMS, :viewers)) + middleware(M.ViewerDidConvert) + end + # fields for: favorite count, favorited_users, viewer_did_favorite.. favorite_fields(:post) star_fields(:post) diff --git a/priv/repo/migrations/20181023142819_create_posts_viewers.exs b/priv/repo/migrations/20181023142819_create_posts_viewers.exs new file mode 100644 index 000000000..a2cc93757 --- /dev/null +++ b/priv/repo/migrations/20181023142819_create_posts_viewers.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreatePostsViewers do + use Ecto.Migration + + def change do + create table(:posts_viewers) do + add(:post_id, references(:cms_posts, on_delete: :delete_all), null: false) + add(:user_id, references(:users, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:posts_viewers, [:post_id, :user_id])) + end +end diff --git a/test/mastani_server_web/mutation/cms/post_test.exs b/test/mastani_server_web/mutation/cms/post_test.exs index 5f3f514e0..b9ed8210e 100644 --- a/test/mastani_server_web/mutation/cms/post_test.exs +++ b/test/mastani_server_web/mutation/cms/post_test.exs @@ -132,7 +132,6 @@ defmodule MastaniServer.Test.Mutation.Post do assert guest_conn |> mutation_get_error?(@query, variables, ecode(:account_login)) end - @tag :wip test "post can be update by owner", ~m(owner_conn post)a do unique_num = System.unique_integer([:positive, :monotonic]) diff --git a/test/mastani_server_web/query/cms/post_test.exs b/test/mastani_server_web/query/cms/post_test.exs index 63d25f0aa..2943212c6 100644 --- a/test/mastani_server_web/query/cms/post_test.exs +++ b/test/mastani_server_web/query/cms/post_test.exs @@ -57,22 +57,6 @@ defmodule MastaniServer.Test.Query.Post do assert is_valid_kv?(results, "favoritedUsers", :list) end - @query """ - query($id: ID!) { - post(id: $id) { - views - } - } - """ - test "views should +1 after query the post", ~m(user_conn post)a do - variables = %{id: post.id} - views_1 = user_conn |> query_result(@query, variables, "post") |> Map.get("views") - - variables = %{id: post.id} - views_2 = user_conn |> query_result(@query, variables, "post") |> Map.get("views") - assert views_2 == views_1 + 1 - end - @query """ query($id: ID!) { post(id: $id) { diff --git a/test/mastani_server_web/query/cms/post_viewer_test.exs b/test/mastani_server_web/query/cms/post_viewer_test.exs new file mode 100644 index 000000000..5fc7a0fdc --- /dev/null +++ b/test/mastani_server_web/query/cms/post_viewer_test.exs @@ -0,0 +1,105 @@ +defmodule MastaniServer.Test.Query.PostViewer do + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.CMS + + setup do + {:ok, community} = db_insert(:community) + {:ok, user} = db_insert(:user) + {:ok, post} = CMS.create_content(community, :post, mock_attrs(:post), user) + # noise + {:ok, post2} = CMS.create_content(community, :post, mock_attrs(:post), user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user) + + {:ok, ~m(user_conn guest_conn community post post2)a} + end + + @query """ + query($id: ID!) { + post(id: $id) { + views + } + } + """ + @tag :wip + test "guest user views should +1 after query the post", ~m(guest_conn post)a do + variables = %{id: post.id} + views_1 = guest_conn |> query_result(@query, variables, "post") |> Map.get("views") + + variables = %{id: post.id} + views_2 = guest_conn |> query_result(@query, variables, "post") |> Map.get("views") + assert views_2 == views_1 + 1 + end + + @tag :wip + test "login views should +1 after query the post", ~m(user_conn post)a do + variables = %{id: post.id} + views_1 = user_conn |> query_result(@query, variables, "post") |> Map.get("views") + + variables = %{id: post.id} + views_2 = user_conn |> query_result(@query, variables, "post") |> Map.get("views") + assert views_2 == views_1 + 1 + end + + @tag :wip + test "login views be record only once in post viewers", ~m(post)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + assert {:error, _} = ORM.find_by(CMS.PostViewer, %{post_id: post.id, user_id: user.id}) + + variables = %{id: post.id} + user_conn |> query_result(@query, variables, "post") |> Map.get("views") + assert {:ok, viewer} = ORM.find_by(CMS.PostViewer, %{post_id: post.id, user_id: user.id}) + assert viewer.post_id == post.id + assert viewer.user_id == user.id + + variables = %{id: post.id} + user_conn |> query_result(@query, variables, "post") |> Map.get("views") + assert {:ok, _} = ORM.find_by(CMS.PostViewer, %{post_id: post.id, user_id: user.id}) + assert viewer.post_id == post.id + assert viewer.user_id == user.id + end + + @paged_query """ + query($filter: PagedArticleFilter!) { + pagedPosts(filter: $filter) { + entries { + id + views + viewerHasViewed + } + } + } + """ + + @query """ + query($id: ID!) { + post(id: $id) { + id + views + viewerHasViewed + } + } + """ + @tag :wip + test "user get has viewed flag after query/read the post", ~m(user_conn community post)a do + variables = %{filter: %{community: community.raw}} + results = user_conn |> query_result(@paged_query, variables, "pagedPosts") + found = Enum.find(results["entries"], &(&1["id"] == to_string(post.id))) + assert found["viewerHasViewed"] == false + + variables = %{id: post.id} + result = user_conn |> query_result(@query, variables, "post") + assert result["viewerHasViewed"] == true + + variables = %{filter: %{community: community.raw}} + results = user_conn |> query_result(@paged_query, variables, "pagedPosts") + + found = Enum.find(results["entries"], &(&1["id"] == to_string(post.id))) + assert found["viewerHasViewed"] == true + end +end From d149449ce0ea9834f965bbc4acf3ba92c0b296be Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 23 Oct 2018 23:49:11 +0800 Subject: [PATCH 098/129] feat(has_viewed): add for jobs --- lib/mastani_server/cms/job.ex | 2 + lib/mastani_server/cms/job_viewer.ex | 27 +++++ lib/mastani_server/cms/utils/loader.ex | 6 + lib/mastani_server/cms/utils/matcher.ex | 4 +- .../resolvers/cms_resolver.ex | 8 +- .../schema/cms/cms_types.ex | 11 +- lib/mastani_server_web/schema/utils/helper.ex | 13 +++ .../20181023153633_create_jobs_viewers.exs | 14 +++ .../mastani_server_web/query/cms/job_test.exs | 16 --- .../query/cms/job_viewer_test.exs | 105 ++++++++++++++++++ 10 files changed, 178 insertions(+), 28 deletions(-) create mode 100644 lib/mastani_server/cms/job_viewer.ex create mode 100644 priv/repo/migrations/20181023153633_create_jobs_viewers.exs create mode 100644 test/mastani_server_web/query/cms/job_viewer_test.exs diff --git a/lib/mastani_server/cms/job.ex b/lib/mastani_server/cms/job.ex index c8d21d8e1..98ecf368f 100644 --- a/lib/mastani_server/cms/job.ex +++ b/lib/mastani_server/cms/job.ex @@ -11,6 +11,7 @@ defmodule MastaniServer.CMS.Job do JobComment, JobFavorite, JobStar, + JobViewer, JobCommunityFlag, Tag } @@ -53,6 +54,7 @@ defmodule MastaniServer.CMS.Job do has_many(:comments, {"jobs_comments", JobComment}) has_many(:favorites, {"jobs_favorites", JobFavorite}) has_many(:stars, {"jobs_stars", JobStar}) + has_many(:viewers, {"jobs_viewers", JobViewer}) many_to_many( :tags, diff --git a/lib/mastani_server/cms/job_viewer.ex b/lib/mastani_server/cms/job_viewer.ex new file mode 100644 index 000000000..f922767e3 --- /dev/null +++ b/lib/mastani_server/cms/job_viewer.ex @@ -0,0 +1,27 @@ +defmodule MastaniServer.CMS.JobViewer do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.Job + + @required_fields ~w(job_id user_id)a + + @type t :: %JobViewer{} + schema "jobs_viewers" do + belongs_to(:job, Job, foreign_key: :job_id) + belongs_to(:user, Accounts.User, foreign_key: :user_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%JobViewer{} = job_viewer, attrs) do + job_viewer + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> unique_constraint(:user_id, name: :jobs_viewers_job_id_user_id_index) + end +end diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index f0c6c3f8b..9b045ab95 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -23,6 +23,7 @@ defmodule MastaniServer.CMS.Utils.Loader do PostStar, # JOB # Job, + JobViewer, JobFavorite, # JobStar, JobComment, @@ -99,6 +100,11 @@ defmodule MastaniServer.CMS.Utils.Loader do PostViewer |> where([pv], pv.user_id == ^cur_user.id) end + # TODO: move later + def query({"jobs_viewers", JobViewer}, %{cur_user: cur_user}) do + JobViewer |> where([pv], pv.user_id == ^cur_user.id) + end + @doc """ handle query: 1. bacic filter of pagi,when,sort ... diff --git a/lib/mastani_server/cms/utils/matcher.ex b/lib/mastani_server/cms/utils/matcher.ex index 7f03be2df..9a9114d3c 100644 --- a/lib/mastani_server/cms/utils/matcher.ex +++ b/lib/mastani_server/cms/utils/matcher.ex @@ -13,6 +13,7 @@ defmodule MastaniServer.CMS.Utils.Matcher do Job, # viewer PostViewer, + JobViewer, # reactions PostFavorite, JobFavorite, @@ -86,7 +87,8 @@ defmodule MastaniServer.CMS.Utils.Matcher do ######################################### ## jobs ... ######################################### - def match_action(:job, :self), do: {:ok, %{target: Job, reactor: Job, preload: :author}} + def match_action(:job, :self), + do: {:ok, %{target: Job, reactor: Job, preload: :author, viewer: JobViewer}} def match_action(:job, :community), do: {:ok, %{target: Job, reactor: Community, flag: JobCommunityFlag}} diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 75a9ecea0..c5ce2d78b 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -32,16 +32,20 @@ defmodule MastaniServerWeb.Resolvers.CMS do # ####################### # community thread (post, job) # ####################### - # def post(_root, %{id: id}, _info), do: Post |> ORM.read(id, inc: :views) def post(_root, %{id: id}, %{context: %{cur_user: user}}) do CMS.read_content(:post, id, user) end def post(_root, %{id: id}, _info), do: Post |> ORM.read(id, inc: :views) + def job(_root, %{id: id}, %{context: %{cur_user: user}}) do + CMS.read_content(:job, id, user) + end + + def job(_root, %{id: id}, _info), do: Job |> ORM.read(id, inc: :views) + def video(_root, %{id: id}, _info), do: Video |> ORM.read(id, inc: :views) def repo(_root, %{id: id}, _info), do: Repo |> ORM.read(id, inc: :views) - def job(_root, %{id: id}, _info), do: Job |> ORM.read(id, inc: :views) def wiki(_root, ~m(community)a, _info), do: CMS.get_wiki(%Community{raw: community}) def cheatsheet(_root, ~m(community)a, _info), do: CMS.get_cheatsheet(%Community{raw: community}) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 0e7fec75d..8b1724159 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -57,15 +57,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do end) end - @desc "if user has viewed this post" - field :viewer_has_viewed, :boolean do - middleware(M.Authorize, :login) - middleware(M.PutCurrentUser) - - resolve(dataloader(CMS, :viewers)) - middleware(M.ViewerDidConvert) - end - + has_viewed_field() # fields for: favorite count, favorited_users, viewer_did_favorite.. favorite_fields(:post) star_fields(:post) @@ -98,6 +90,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # comments_participators comments_counter_fields(:job) + has_viewed_field() # fields for: favorite count, favorited_users, viewer_did_favorite.. favorite_fields(:job) timestamp_fields() diff --git a/lib/mastani_server_web/schema/utils/helper.ex b/lib/mastani_server_web/schema/utils/helper.ex index 1c3c255ce..2b1e88818 100644 --- a/lib/mastani_server_web/schema/utils/helper.ex +++ b/lib/mastani_server_web/schema/utils/helper.ex @@ -53,6 +53,19 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do alias MastaniServerWeb.Resolvers, as: R alias MastaniServerWeb.Middleware, as: M + defmacro has_viewed_field do + quote do + # @desc "if user has viewed this content" + field :viewer_has_viewed, :boolean do + middleware(M.Authorize, :login) + middleware(M.PutCurrentUser) + + resolve(dataloader(CMS, :viewers)) + middleware(M.ViewerDidConvert) + end + end + end + # fields for: favorite count, favorited_users, viewer_did_favorite.. defmacro favorite_fields(thread) do quote do diff --git a/priv/repo/migrations/20181023153633_create_jobs_viewers.exs b/priv/repo/migrations/20181023153633_create_jobs_viewers.exs new file mode 100644 index 000000000..a15b39838 --- /dev/null +++ b/priv/repo/migrations/20181023153633_create_jobs_viewers.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreateJobsViewers do + use Ecto.Migration + + def change do + create table(:jobs_viewers) do + add(:job_id, references(:cms_jobs, on_delete: :delete_all), null: false) + add(:user_id, references(:users, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:jobs_viewers, [:job_id, :user_id])) + end +end diff --git a/test/mastani_server_web/query/cms/job_test.exs b/test/mastani_server_web/query/cms/job_test.exs index 7e6536928..fd7cc23a5 100644 --- a/test/mastani_server_web/query/cms/job_test.exs +++ b/test/mastani_server_web/query/cms/job_test.exs @@ -57,22 +57,6 @@ defmodule MastaniServer.Test.Query.Job do assert is_valid_kv?(results, "favoritedUsers", :list) end - @query """ - query($id: ID!) { - job(id: $id) { - views - } - } - """ - test "views should +1 after query the job", ~m(user_conn job)a do - variables = %{id: job.id} - views_1 = user_conn |> query_result(@query, variables, "job") |> Map.get("views") - - variables = %{id: job.id} - views_2 = user_conn |> query_result(@query, variables, "job") |> Map.get("views") - assert views_2 == views_1 + 1 - end - @query """ query($id: ID!) { job(id: $id) { diff --git a/test/mastani_server_web/query/cms/job_viewer_test.exs b/test/mastani_server_web/query/cms/job_viewer_test.exs new file mode 100644 index 000000000..c7438205c --- /dev/null +++ b/test/mastani_server_web/query/cms/job_viewer_test.exs @@ -0,0 +1,105 @@ +defmodule MastaniServer.Test.Query.JobViewer do + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.CMS + + setup do + {:ok, community} = db_insert(:community) + {:ok, user} = db_insert(:user) + {:ok, job} = CMS.create_content(community, :job, mock_attrs(:job), user) + # noise + {:ok, job2} = CMS.create_content(community, :job, mock_attrs(:job), user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user) + + {:ok, ~m(user_conn guest_conn community job job2)a} + end + + @query """ + query($id: ID!) { + job(id: $id) { + views + } + } + """ + @tag :wip + test "guest user views should +1 after query the job", ~m(guest_conn job)a do + variables = %{id: job.id} + views_1 = guest_conn |> query_result(@query, variables, "job") |> Map.get("views") + + variables = %{id: job.id} + views_2 = guest_conn |> query_result(@query, variables, "job") |> Map.get("views") + assert views_2 == views_1 + 1 + end + + @tag :wip + test "login views should +1 after query the job", ~m(user_conn job)a do + variables = %{id: job.id} + views_1 = user_conn |> query_result(@query, variables, "job") |> Map.get("views") + + variables = %{id: job.id} + views_2 = user_conn |> query_result(@query, variables, "job") |> Map.get("views") + assert views_2 == views_1 + 1 + end + + @tag :wip + test "login views be record only once in job viewers", ~m(job)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + assert {:error, _} = ORM.find_by(CMS.JobViewer, %{job_id: job.id, user_id: user.id}) + + variables = %{id: job.id} + user_conn |> query_result(@query, variables, "job") |> Map.get("views") + assert {:ok, viewer} = ORM.find_by(CMS.JobViewer, %{job_id: job.id, user_id: user.id}) + assert viewer.job_id == job.id + assert viewer.user_id == user.id + + variables = %{id: job.id} + user_conn |> query_result(@query, variables, "job") |> Map.get("views") + assert {:ok, _} = ORM.find_by(CMS.JobViewer, %{job_id: job.id, user_id: user.id}) + assert viewer.job_id == job.id + assert viewer.user_id == user.id + end + + @paged_query """ + query($filter: PagedArticleFilter!) { + pagedJobs(filter: $filter) { + entries { + id + views + viewerHasViewed + } + } + } + """ + + @query """ + query($id: ID!) { + job(id: $id) { + id + views + viewerHasViewed + } + } + """ + @tag :wip + test "user get has viewed flag after query/read the job", ~m(user_conn community job)a do + variables = %{filter: %{community: community.raw}} + results = user_conn |> query_result(@paged_query, variables, "pagedJobs") + found = Enum.find(results["entries"], &(&1["id"] == to_string(job.id))) + assert found["viewerHasViewed"] == false + + variables = %{id: job.id} + result = user_conn |> query_result(@query, variables, "job") + assert result["viewerHasViewed"] == true + + variables = %{filter: %{community: community.raw}} + results = user_conn |> query_result(@paged_query, variables, "pagedJobs") + + found = Enum.find(results["entries"], &(&1["id"] == to_string(job.id))) + assert found["viewerHasViewed"] == true + end +end From b428bc85fcb151e68b441d448dfbae6ef51e4aae Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 24 Oct 2018 00:00:11 +0800 Subject: [PATCH 099/129] feat(viewer_has_viewed): for videos --- lib/mastani_server/cms/utils/matcher.ex | 4 +- lib/mastani_server/cms/video.ex | 2 + lib/mastani_server/cms/video_viewer.ex | 27 +++++ .../resolvers/cms_resolver.ex | 5 + .../schema/cms/cms_types.ex | 1 + .../20181023154929_create_videos_viewers.exs | 14 +++ .../query/cms/video_test.exs | 15 --- .../query/cms/video_viewer_test.exs | 105 ++++++++++++++++++ 8 files changed, 157 insertions(+), 16 deletions(-) create mode 100644 lib/mastani_server/cms/video_viewer.ex create mode 100644 priv/repo/migrations/20181023154929_create_videos_viewers.exs create mode 100644 test/mastani_server_web/query/cms/video_viewer_test.exs diff --git a/lib/mastani_server/cms/utils/matcher.ex b/lib/mastani_server/cms/utils/matcher.ex index 9a9114d3c..85ff97ede 100644 --- a/lib/mastani_server/cms/utils/matcher.ex +++ b/lib/mastani_server/cms/utils/matcher.ex @@ -14,6 +14,7 @@ defmodule MastaniServer.CMS.Utils.Matcher do # viewer PostViewer, JobViewer, + VideoViewer, # reactions PostFavorite, JobFavorite, @@ -111,7 +112,8 @@ defmodule MastaniServer.CMS.Utils.Matcher do ######################################### ## videos ... ######################################### - def match_action(:video, :self), do: {:ok, %{target: Video, reactor: Video, preload: :author}} + def match_action(:video, :self), + do: {:ok, %{target: Video, reactor: Video, preload: :author, viewer: VideoViewer}} def match_action(:video, :community), do: {:ok, %{target: Video, reactor: Community, flag: VideoCommunityFlag}} diff --git a/lib/mastani_server/cms/video.ex b/lib/mastani_server/cms/video.ex index 6d6b920c9..dc9664ee5 100644 --- a/lib/mastani_server/cms/video.ex +++ b/lib/mastani_server/cms/video.ex @@ -12,6 +12,7 @@ defmodule MastaniServer.CMS.Video do VideoFavorite, VideoCommunityFlag, VideoStar, + VideoViewer, Tag } @@ -44,6 +45,7 @@ defmodule MastaniServer.CMS.Video do has_many(:favorites, {"videos_favorites", VideoFavorite}) has_many(:stars, {"videos_stars", VideoStar}) + has_many(:viewers, {"videos_viewers", VideoViewer}) has_many(:comments, {"videos_comments", VideoComment}) many_to_many( diff --git a/lib/mastani_server/cms/video_viewer.ex b/lib/mastani_server/cms/video_viewer.ex new file mode 100644 index 000000000..1b02e9e79 --- /dev/null +++ b/lib/mastani_server/cms/video_viewer.ex @@ -0,0 +1,27 @@ +defmodule MastaniServer.CMS.VideoViewer do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.Video + + @required_fields ~w(video_id user_id)a + + @type t :: %VideoViewer{} + schema "videos_viewers" do + belongs_to(:video, Video, foreign_key: :video_id) + belongs_to(:user, Accounts.User, foreign_key: :user_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%VideoViewer{} = video_viewer, attrs) do + video_viewer + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> unique_constraint(:user_id, name: :videos_viewers_video_id_user_id_index) + end +end diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index c5ce2d78b..eaf4ef940 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -44,7 +44,12 @@ defmodule MastaniServerWeb.Resolvers.CMS do def job(_root, %{id: id}, _info), do: Job |> ORM.read(id, inc: :views) + def video(_root, %{id: id}, %{context: %{cur_user: user}}) do + CMS.read_content(:video, id, user) + end + def video(_root, %{id: id}, _info), do: Video |> ORM.read(id, inc: :views) + def repo(_root, %{id: id}, _info), do: Repo |> ORM.read(id, inc: :views) def wiki(_root, ~m(community)a, _info), do: CMS.get_wiki(%Community{raw: community}) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 8b1724159..f6c4ef4cd 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -123,6 +123,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do # comments_participators comments_counter_fields(:video) + has_viewed_field() # fields for: favorite count, favorited_users, viewer_did_favorite.. favorite_fields(:video) star_fields(:video) diff --git a/priv/repo/migrations/20181023154929_create_videos_viewers.exs b/priv/repo/migrations/20181023154929_create_videos_viewers.exs new file mode 100644 index 000000000..f1fd6bd3e --- /dev/null +++ b/priv/repo/migrations/20181023154929_create_videos_viewers.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreateVideosViewers do + use Ecto.Migration + + def change do + create table(:videos_viewers) do + add(:video_id, references(:cms_videos, on_delete: :delete_all), null: false) + add(:user_id, references(:users, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:videos_viewers, [:video_id, :user_id])) + end +end diff --git a/test/mastani_server_web/query/cms/video_test.exs b/test/mastani_server_web/query/cms/video_test.exs index 9e9c57f1c..d0de66506 100644 --- a/test/mastani_server_web/query/cms/video_test.exs +++ b/test/mastani_server_web/query/cms/video_test.exs @@ -27,21 +27,6 @@ defmodule MastaniServer.Test.Query.Video do assert length(Map.keys(results)) == 2 end - @query """ - query($id: ID!) { - video(id: $id) { - views - } - } - """ - test "views should +1 after query the video", ~m(user_conn video)a do - variables = %{id: video.id} - views_1 = user_conn |> query_result(@query, variables, "video") |> Map.get("views") - variables = %{id: video.id} - views_2 = user_conn |> query_result(@query, variables, "video") |> Map.get("views") - assert views_2 == views_1 + 1 - end - alias MastaniServer.Accounts @query """ diff --git a/test/mastani_server_web/query/cms/video_viewer_test.exs b/test/mastani_server_web/query/cms/video_viewer_test.exs new file mode 100644 index 000000000..ba4a6a06a --- /dev/null +++ b/test/mastani_server_web/query/cms/video_viewer_test.exs @@ -0,0 +1,105 @@ +defmodule MastaniServer.Test.Query.VideoViewer do + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.CMS + + setup do + {:ok, community} = db_insert(:community) + {:ok, user} = db_insert(:user) + {:ok, video} = CMS.create_content(community, :video, mock_attrs(:video), user) + # noise + {:ok, video2} = CMS.create_content(community, :video, mock_attrs(:video), user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user) + + {:ok, ~m(user_conn guest_conn community video video2)a} + end + + @query """ + query($id: ID!) { + video(id: $id) { + views + } + } + """ + @tag :wip + test "guest user views should +1 after query the video", ~m(guest_conn video)a do + variables = %{id: video.id} + views_1 = guest_conn |> query_result(@query, variables, "video") |> Map.get("views") + + variables = %{id: video.id} + views_2 = guest_conn |> query_result(@query, variables, "video") |> Map.get("views") + assert views_2 == views_1 + 1 + end + + @tag :wip + test "login views should +1 after query the video", ~m(user_conn video)a do + variables = %{id: video.id} + views_1 = user_conn |> query_result(@query, variables, "video") |> Map.get("views") + + variables = %{id: video.id} + views_2 = user_conn |> query_result(@query, variables, "video") |> Map.get("views") + assert views_2 == views_1 + 1 + end + + @tag :wip + test "login views be record only once in video viewers", ~m(video)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + assert {:error, _} = ORM.find_by(CMS.VideoViewer, %{video_id: video.id, user_id: user.id}) + + variables = %{id: video.id} + user_conn |> query_result(@query, variables, "video") |> Map.get("views") + assert {:ok, viewer} = ORM.find_by(CMS.VideoViewer, %{video_id: video.id, user_id: user.id}) + assert viewer.video_id == video.id + assert viewer.user_id == user.id + + variables = %{id: video.id} + user_conn |> query_result(@query, variables, "video") |> Map.get("views") + assert {:ok, _} = ORM.find_by(CMS.VideoViewer, %{video_id: video.id, user_id: user.id}) + assert viewer.video_id == video.id + assert viewer.user_id == user.id + end + + @paged_query """ + query($filter: PagedArticleFilter!) { + pagedVideos(filter: $filter) { + entries { + id + views + viewerHasViewed + } + } + } + """ + + @query """ + query($id: ID!) { + video(id: $id) { + id + views + viewerHasViewed + } + } + """ + @tag :wip + test "user get has viewed flag after query/read the video", ~m(user_conn community video)a do + variables = %{filter: %{community: community.raw}} + results = user_conn |> query_result(@paged_query, variables, "pagedVideos") + found = Enum.find(results["entries"], &(&1["id"] == to_string(video.id))) + assert found["viewerHasViewed"] == false + + variables = %{id: video.id} + result = user_conn |> query_result(@query, variables, "video") + assert result["viewerHasViewed"] == true + + variables = %{filter: %{community: community.raw}} + results = user_conn |> query_result(@paged_query, variables, "pagedVideos") + + found = Enum.find(results["entries"], &(&1["id"] == to_string(video.id))) + assert found["viewerHasViewed"] == true + end +end From 9dcbb1f5fe8f1975a1b07928a4a0f2cb63c7f0ac Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 24 Oct 2018 00:17:11 +0800 Subject: [PATCH 100/129] feat(viewer_has_viewd): for repos --- lib/mastani_server/cms/repo.ex | 2 + lib/mastani_server/cms/repo_viewer.ex | 27 +++++ lib/mastani_server/cms/utils/loader.ex | 11 +- lib/mastani_server/cms/utils/matcher.ex | 4 +- .../resolvers/cms_resolver.ex | 6 +- .../schema/cms/cms_types.ex | 1 + .../20181023160033_create_repos_viewers.exs | 14 +++ .../query/cms/repo_viewer_test.exs | 105 ++++++++++++++++++ 8 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 lib/mastani_server/cms/repo_viewer.ex create mode 100644 priv/repo/migrations/20181023160033_create_repos_viewers.exs create mode 100644 test/mastani_server_web/query/cms/repo_viewer_test.exs diff --git a/lib/mastani_server/cms/repo.ex b/lib/mastani_server/cms/repo.ex index 270e08739..327b1b169 100644 --- a/lib/mastani_server/cms/repo.ex +++ b/lib/mastani_server/cms/repo.ex @@ -11,6 +11,7 @@ defmodule MastaniServer.CMS.Repo do RepoComment, RepoContributor, RepoFavorite, + RepoViewer, RepoLang, RepoCommunityFlag, Tag @@ -54,6 +55,7 @@ defmodule MastaniServer.CMS.Repo do has_many(:comments, {"repos_comments", RepoComment}) has_many(:favorites, {"repos_favorites", RepoFavorite}) + has_many(:viewers, {"repos_viewers", RepoViewer}) many_to_many( :tags, diff --git a/lib/mastani_server/cms/repo_viewer.ex b/lib/mastani_server/cms/repo_viewer.ex new file mode 100644 index 000000000..47597bb37 --- /dev/null +++ b/lib/mastani_server/cms/repo_viewer.ex @@ -0,0 +1,27 @@ +defmodule MastaniServer.CMS.RepoViewer do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.Accounts + alias MastaniServer.CMS.Repo + + @required_fields ~w(repo_id user_id)a + + @type t :: %RepoViewer{} + schema "repos_viewers" do + belongs_to(:repo, Repo, foreign_key: :repo_id) + belongs_to(:user, Accounts.User, foreign_key: :user_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%RepoViewer{} = repo_viewer, attrs) do + repo_viewer + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> unique_constraint(:user_id, name: :repos_viewers_repo_id_user_id_index) + end +end diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index 9b045ab95..35e060f87 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -31,6 +31,7 @@ defmodule MastaniServer.CMS.Utils.Loader do JobCommentDislike, JobCommentLike, # Video + VideoViewer, VideoFavorite, VideoStar, VideoComment, @@ -38,6 +39,7 @@ defmodule MastaniServer.CMS.Utils.Loader do VideoCommentDislike, VideoCommentLike, # repo + RepoViewer, RepoFavorite, RepoComment, RepoCommentReply, @@ -100,11 +102,18 @@ defmodule MastaniServer.CMS.Utils.Loader do PostViewer |> where([pv], pv.user_id == ^cur_user.id) end - # TODO: move later def query({"jobs_viewers", JobViewer}, %{cur_user: cur_user}) do JobViewer |> where([pv], pv.user_id == ^cur_user.id) end + def query({"videos_viewers", VideoViewer}, %{cur_user: cur_user}) do + VideoViewer |> where([pv], pv.user_id == ^cur_user.id) + end + + def query({"repos_viewers", RepoViewer}, %{cur_user: cur_user}) do + RepoViewer |> where([pv], pv.user_id == ^cur_user.id) + end + @doc """ handle query: 1. bacic filter of pagi,when,sort ... diff --git a/lib/mastani_server/cms/utils/matcher.ex b/lib/mastani_server/cms/utils/matcher.ex index 85ff97ede..9e2ad6643 100644 --- a/lib/mastani_server/cms/utils/matcher.ex +++ b/lib/mastani_server/cms/utils/matcher.ex @@ -15,6 +15,7 @@ defmodule MastaniServer.CMS.Utils.Matcher do PostViewer, JobViewer, VideoViewer, + RepoViewer, # reactions PostFavorite, JobFavorite, @@ -136,7 +137,8 @@ defmodule MastaniServer.CMS.Utils.Matcher do ######################################### ## repos ... ######################################### - def match_action(:repo, :self), do: {:ok, %{target: Repo, reactor: Repo, preload: :author}} + def match_action(:repo, :self), + do: {:ok, %{target: Repo, reactor: Repo, preload: :author, viewer: RepoViewer}} def match_action(:repo, :community), do: {:ok, %{target: Repo, reactor: Community, flag: RepoCommunityFlag}} diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index eaf4ef940..5066226d6 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -30,7 +30,7 @@ defmodule MastaniServerWeb.Resolvers.CMS do def delete_community(_root, %{id: id}, _info), do: Community |> ORM.find_delete(id) # ####################### - # community thread (post, job) + # community thread (post, job), login user should be logged # ####################### def post(_root, %{id: id}, %{context: %{cur_user: user}}) do CMS.read_content(:post, id, user) @@ -50,6 +50,10 @@ defmodule MastaniServerWeb.Resolvers.CMS do def video(_root, %{id: id}, _info), do: Video |> ORM.read(id, inc: :views) + def repo(_root, %{id: id}, %{context: %{cur_user: user}}) do + CMS.read_content(:repo, id, user) + end + def repo(_root, %{id: id}, _info), do: Repo |> ORM.read(id, inc: :views) def wiki(_root, ~m(community)a, _info), do: CMS.get_wiki(%Community{raw: community}) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index f6c4ef4cd..035c08890 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -167,6 +167,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + has_viewed_field() # comments_count # comments_participators comments_counter_fields(:repo) diff --git a/priv/repo/migrations/20181023160033_create_repos_viewers.exs b/priv/repo/migrations/20181023160033_create_repos_viewers.exs new file mode 100644 index 000000000..b903513bf --- /dev/null +++ b/priv/repo/migrations/20181023160033_create_repos_viewers.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreateReposViewers do + use Ecto.Migration + + def change do + create table(:repos_viewers) do + add(:repo_id, references(:cms_repos, on_delete: :delete_all), null: false) + add(:user_id, references(:users, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:repos_viewers, [:repo_id, :user_id])) + end +end diff --git a/test/mastani_server_web/query/cms/repo_viewer_test.exs b/test/mastani_server_web/query/cms/repo_viewer_test.exs new file mode 100644 index 000000000..29a902362 --- /dev/null +++ b/test/mastani_server_web/query/cms/repo_viewer_test.exs @@ -0,0 +1,105 @@ +defmodule MastaniServer.Test.Query.RepoViewer do + use MastaniServer.TestTools + + alias Helper.ORM + alias MastaniServer.CMS + + setup do + {:ok, community} = db_insert(:community) + {:ok, user} = db_insert(:user) + {:ok, repo} = CMS.create_content(community, :repo, mock_attrs(:repo), user) + # noise + {:ok, repo2} = CMS.create_content(community, :repo, mock_attrs(:repo), user) + + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user) + + {:ok, ~m(user_conn guest_conn community repo repo2)a} + end + + @query """ + query($id: ID!) { + repo(id: $id) { + views + } + } + """ + @tag :wip + test "guest user views should +1 after query the repo", ~m(guest_conn repo)a do + variables = %{id: repo.id} + views_1 = guest_conn |> query_result(@query, variables, "repo") |> Map.get("views") + + variables = %{id: repo.id} + views_2 = guest_conn |> query_result(@query, variables, "repo") |> Map.get("views") + assert views_2 == views_1 + 1 + end + + @tag :wip + test "login views should +1 after query the repo", ~m(user_conn repo)a do + variables = %{id: repo.id} + views_1 = user_conn |> query_result(@query, variables, "repo") |> Map.get("views") + + variables = %{id: repo.id} + views_2 = user_conn |> query_result(@query, variables, "repo") |> Map.get("views") + assert views_2 == views_1 + 1 + end + + @tag :wip + test "login views be record only once in repo viewers", ~m(repo)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + assert {:error, _} = ORM.find_by(CMS.RepoViewer, %{repo_id: repo.id, user_id: user.id}) + + variables = %{id: repo.id} + user_conn |> query_result(@query, variables, "repo") |> Map.get("views") + assert {:ok, viewer} = ORM.find_by(CMS.RepoViewer, %{repo_id: repo.id, user_id: user.id}) + assert viewer.repo_id == repo.id + assert viewer.user_id == user.id + + variables = %{id: repo.id} + user_conn |> query_result(@query, variables, "repo") |> Map.get("views") + assert {:ok, _} = ORM.find_by(CMS.RepoViewer, %{repo_id: repo.id, user_id: user.id}) + assert viewer.repo_id == repo.id + assert viewer.user_id == user.id + end + + @paged_query """ + query($filter: PagedArticleFilter!) { + pagedRepos(filter: $filter) { + entries { + id + views + viewerHasViewed + } + } + } + """ + + @query """ + query($id: ID!) { + repo(id: $id) { + id + views + viewerHasViewed + } + } + """ + @tag :wip + test "user get has viewed flag after query/read the repo", ~m(user_conn community repo)a do + variables = %{filter: %{community: community.raw}} + results = user_conn |> query_result(@paged_query, variables, "pagedRepos") + found = Enum.find(results["entries"], &(&1["id"] == to_string(repo.id))) + assert found["viewerHasViewed"] == false + + variables = %{id: repo.id} + result = user_conn |> query_result(@query, variables, "repo") + assert result["viewerHasViewed"] == true + + variables = %{filter: %{community: community.raw}} + results = user_conn |> query_result(@paged_query, variables, "pagedRepos") + + found = Enum.find(results["entries"], &(&1["id"] == to_string(repo.id))) + assert found["viewerHasViewed"] == true + end +end From 9a12b5f8fcc0af7d38dfd379bbdf8e0a83b1f051 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 24 Oct 2018 00:21:21 +0800 Subject: [PATCH 101/129] chore(clean up): wip tags --- .../query/cms/job_viewer_test.exs | 4 ---- .../query/cms/post_viewer_test.exs | 4 ---- test/mastani_server_web/query/cms/repo_test.exs | 16 ---------------- .../query/cms/repo_viewer_test.exs | 4 ---- .../query/cms/video_viewer_test.exs | 4 ---- 5 files changed, 32 deletions(-) diff --git a/test/mastani_server_web/query/cms/job_viewer_test.exs b/test/mastani_server_web/query/cms/job_viewer_test.exs index c7438205c..1150352c6 100644 --- a/test/mastani_server_web/query/cms/job_viewer_test.exs +++ b/test/mastani_server_web/query/cms/job_viewer_test.exs @@ -24,7 +24,6 @@ defmodule MastaniServer.Test.Query.JobViewer do } } """ - @tag :wip test "guest user views should +1 after query the job", ~m(guest_conn job)a do variables = %{id: job.id} views_1 = guest_conn |> query_result(@query, variables, "job") |> Map.get("views") @@ -34,7 +33,6 @@ defmodule MastaniServer.Test.Query.JobViewer do assert views_2 == views_1 + 1 end - @tag :wip test "login views should +1 after query the job", ~m(user_conn job)a do variables = %{id: job.id} views_1 = user_conn |> query_result(@query, variables, "job") |> Map.get("views") @@ -44,7 +42,6 @@ defmodule MastaniServer.Test.Query.JobViewer do assert views_2 == views_1 + 1 end - @tag :wip test "login views be record only once in job viewers", ~m(job)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -85,7 +82,6 @@ defmodule MastaniServer.Test.Query.JobViewer do } } """ - @tag :wip test "user get has viewed flag after query/read the job", ~m(user_conn community job)a do variables = %{filter: %{community: community.raw}} results = user_conn |> query_result(@paged_query, variables, "pagedJobs") diff --git a/test/mastani_server_web/query/cms/post_viewer_test.exs b/test/mastani_server_web/query/cms/post_viewer_test.exs index 5fc7a0fdc..dfacf47ec 100644 --- a/test/mastani_server_web/query/cms/post_viewer_test.exs +++ b/test/mastani_server_web/query/cms/post_viewer_test.exs @@ -24,7 +24,6 @@ defmodule MastaniServer.Test.Query.PostViewer do } } """ - @tag :wip test "guest user views should +1 after query the post", ~m(guest_conn post)a do variables = %{id: post.id} views_1 = guest_conn |> query_result(@query, variables, "post") |> Map.get("views") @@ -34,7 +33,6 @@ defmodule MastaniServer.Test.Query.PostViewer do assert views_2 == views_1 + 1 end - @tag :wip test "login views should +1 after query the post", ~m(user_conn post)a do variables = %{id: post.id} views_1 = user_conn |> query_result(@query, variables, "post") |> Map.get("views") @@ -44,7 +42,6 @@ defmodule MastaniServer.Test.Query.PostViewer do assert views_2 == views_1 + 1 end - @tag :wip test "login views be record only once in post viewers", ~m(post)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -85,7 +82,6 @@ defmodule MastaniServer.Test.Query.PostViewer do } } """ - @tag :wip test "user get has viewed flag after query/read the post", ~m(user_conn community post)a do variables = %{filter: %{community: community.raw}} results = user_conn |> query_result(@paged_query, variables, "pagedPosts") diff --git a/test/mastani_server_web/query/cms/repo_test.exs b/test/mastani_server_web/query/cms/repo_test.exs index c855d75e6..c7905020a 100644 --- a/test/mastani_server_web/query/cms/repo_test.exs +++ b/test/mastani_server_web/query/cms/repo_test.exs @@ -57,22 +57,6 @@ defmodule MastaniServer.Test.Query.Repo do assert is_valid_kv?(results, "favoritedUsers", :list) end - @query """ - query($id: ID!) { - repo(id: $id) { - views - } - } - """ - test "views should +1 after query the repo", ~m(user_conn repo)a do - variables = %{id: repo.id} - views_1 = user_conn |> query_result(@query, variables, "repo") |> Map.get("views") - - variables = %{id: repo.id} - views_2 = user_conn |> query_result(@query, variables, "repo") |> Map.get("views") - assert views_2 == views_1 + 1 - end - @query """ query($id: ID!) { repo(id: $id) { diff --git a/test/mastani_server_web/query/cms/repo_viewer_test.exs b/test/mastani_server_web/query/cms/repo_viewer_test.exs index 29a902362..bb80a5f71 100644 --- a/test/mastani_server_web/query/cms/repo_viewer_test.exs +++ b/test/mastani_server_web/query/cms/repo_viewer_test.exs @@ -24,7 +24,6 @@ defmodule MastaniServer.Test.Query.RepoViewer do } } """ - @tag :wip test "guest user views should +1 after query the repo", ~m(guest_conn repo)a do variables = %{id: repo.id} views_1 = guest_conn |> query_result(@query, variables, "repo") |> Map.get("views") @@ -34,7 +33,6 @@ defmodule MastaniServer.Test.Query.RepoViewer do assert views_2 == views_1 + 1 end - @tag :wip test "login views should +1 after query the repo", ~m(user_conn repo)a do variables = %{id: repo.id} views_1 = user_conn |> query_result(@query, variables, "repo") |> Map.get("views") @@ -44,7 +42,6 @@ defmodule MastaniServer.Test.Query.RepoViewer do assert views_2 == views_1 + 1 end - @tag :wip test "login views be record only once in repo viewers", ~m(repo)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -85,7 +82,6 @@ defmodule MastaniServer.Test.Query.RepoViewer do } } """ - @tag :wip test "user get has viewed flag after query/read the repo", ~m(user_conn community repo)a do variables = %{filter: %{community: community.raw}} results = user_conn |> query_result(@paged_query, variables, "pagedRepos") diff --git a/test/mastani_server_web/query/cms/video_viewer_test.exs b/test/mastani_server_web/query/cms/video_viewer_test.exs index ba4a6a06a..779ef150a 100644 --- a/test/mastani_server_web/query/cms/video_viewer_test.exs +++ b/test/mastani_server_web/query/cms/video_viewer_test.exs @@ -24,7 +24,6 @@ defmodule MastaniServer.Test.Query.VideoViewer do } } """ - @tag :wip test "guest user views should +1 after query the video", ~m(guest_conn video)a do variables = %{id: video.id} views_1 = guest_conn |> query_result(@query, variables, "video") |> Map.get("views") @@ -34,7 +33,6 @@ defmodule MastaniServer.Test.Query.VideoViewer do assert views_2 == views_1 + 1 end - @tag :wip test "login views should +1 after query the video", ~m(user_conn video)a do variables = %{id: video.id} views_1 = user_conn |> query_result(@query, variables, "video") |> Map.get("views") @@ -44,7 +42,6 @@ defmodule MastaniServer.Test.Query.VideoViewer do assert views_2 == views_1 + 1 end - @tag :wip test "login views be record only once in video viewers", ~m(video)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -85,7 +82,6 @@ defmodule MastaniServer.Test.Query.VideoViewer do } } """ - @tag :wip test "user get has viewed flag after query/read the video", ~m(user_conn community video)a do variables = %{filter: %{community: community.raw}} results = user_conn |> query_result(@paged_query, variables, "pagedVideos") From 8902126d307ed98d558640b5e4a1521ce7c0810d Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 24 Oct 2018 08:08:50 +0800 Subject: [PATCH 102/129] fix(contents viewer): add edge case to test dataloader --- test/mastani_server_web/query/cms/job_viewer_test.exs | 8 ++++++++ test/mastani_server_web/query/cms/post_viewer_test.exs | 8 ++++++++ test/mastani_server_web/query/cms/repo_viewer_test.exs | 8 ++++++++ test/mastani_server_web/query/cms/video_viewer_test.exs | 8 ++++++++ 4 files changed, 32 insertions(+) diff --git a/test/mastani_server_web/query/cms/job_viewer_test.exs b/test/mastani_server_web/query/cms/job_viewer_test.exs index 1150352c6..8d9579e1c 100644 --- a/test/mastani_server_web/query/cms/job_viewer_test.exs +++ b/test/mastani_server_web/query/cms/job_viewer_test.exs @@ -92,6 +92,14 @@ defmodule MastaniServer.Test.Query.JobViewer do result = user_conn |> query_result(@query, variables, "job") assert result["viewerHasViewed"] == true + # noise: test viewer dataloader + {:ok, user2} = db_insert(:user) + user_conn2 = simu_conn(:user, user2) + variables = %{filter: %{community: community.raw}} + results = user_conn2 |> query_result(@paged_query, variables, "pagedJobs") + found = Enum.find(results["entries"], &(&1["id"] == to_string(job.id))) + assert found["viewerHasViewed"] == false + variables = %{filter: %{community: community.raw}} results = user_conn |> query_result(@paged_query, variables, "pagedJobs") diff --git a/test/mastani_server_web/query/cms/post_viewer_test.exs b/test/mastani_server_web/query/cms/post_viewer_test.exs index dfacf47ec..ef3516f41 100644 --- a/test/mastani_server_web/query/cms/post_viewer_test.exs +++ b/test/mastani_server_web/query/cms/post_viewer_test.exs @@ -92,6 +92,14 @@ defmodule MastaniServer.Test.Query.PostViewer do result = user_conn |> query_result(@query, variables, "post") assert result["viewerHasViewed"] == true + # noise: test viewer dataloader + {:ok, user2} = db_insert(:user) + user_conn2 = simu_conn(:user, user2) + variables = %{filter: %{community: community.raw}} + results = user_conn2 |> query_result(@paged_query, variables, "pagedPosts") + found = Enum.find(results["entries"], &(&1["id"] == to_string(post.id))) + assert found["viewerHasViewed"] == false + variables = %{filter: %{community: community.raw}} results = user_conn |> query_result(@paged_query, variables, "pagedPosts") diff --git a/test/mastani_server_web/query/cms/repo_viewer_test.exs b/test/mastani_server_web/query/cms/repo_viewer_test.exs index bb80a5f71..48e184927 100644 --- a/test/mastani_server_web/query/cms/repo_viewer_test.exs +++ b/test/mastani_server_web/query/cms/repo_viewer_test.exs @@ -92,6 +92,14 @@ defmodule MastaniServer.Test.Query.RepoViewer do result = user_conn |> query_result(@query, variables, "repo") assert result["viewerHasViewed"] == true + # noise: test viewer dataloader + {:ok, user2} = db_insert(:user) + user_conn2 = simu_conn(:user, user2) + variables = %{filter: %{community: community.raw}} + results = user_conn2 |> query_result(@paged_query, variables, "pagedRepos") + found = Enum.find(results["entries"], &(&1["id"] == to_string(repo.id))) + assert found["viewerHasViewed"] == false + variables = %{filter: %{community: community.raw}} results = user_conn |> query_result(@paged_query, variables, "pagedRepos") diff --git a/test/mastani_server_web/query/cms/video_viewer_test.exs b/test/mastani_server_web/query/cms/video_viewer_test.exs index 779ef150a..fdc0e07d7 100644 --- a/test/mastani_server_web/query/cms/video_viewer_test.exs +++ b/test/mastani_server_web/query/cms/video_viewer_test.exs @@ -92,6 +92,14 @@ defmodule MastaniServer.Test.Query.VideoViewer do result = user_conn |> query_result(@query, variables, "video") assert result["viewerHasViewed"] == true + # noise: test viewer dataloader + {:ok, user2} = db_insert(:user) + user_conn2 = simu_conn(:user, user2) + variables = %{filter: %{community: community.raw}} + results = user_conn2 |> query_result(@paged_query, variables, "pagedVideos") + found = Enum.find(results["entries"], &(&1["id"] == to_string(video.id))) + assert found["viewerHasViewed"] == false + variables = %{filter: %{community: community.raw}} results = user_conn |> query_result(@paged_query, variables, "pagedVideos") From c209f341880d26dca23bc511c84c5bfd2f18bacf Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 24 Oct 2018 14:08:27 +0800 Subject: [PATCH 103/129] refactor(tests): improve with multi argss --- lib/mastani_server/cms/job.ex | 2 +- .../schema/cms/cms_types.ex | 6 ++++ .../schema/cms/mutations/job.ex | 14 ++++++++ .../mutation/cms/job_test.exs | 34 +++++++++++++++---- test/support/factory.ex | 5 +++ 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/lib/mastani_server/cms/job.ex b/lib/mastani_server/cms/job.ex index 98ecf368f..f6df540cf 100644 --- a/lib/mastani_server/cms/job.ex +++ b/lib/mastani_server/cms/job.ex @@ -17,7 +17,7 @@ defmodule MastaniServer.CMS.Job do } @required_fields ~w(title company company_logo location body digest length)a - @optional_fields ~w(link_addr link_source min_education)a + @optional_fields ~w(link_addr link_source min_salary max_salary min_experience max_experience min_education)a @type t :: %Job{} schema "cms_jobs" do diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 035c08890..118f69009 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -86,6 +86,12 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + field(:min_salary, :integer) + field(:max_salary, :integer) + field(:min_experience, :integer) + field(:max_experience, :integer) + field(:min_education, :string) + # comments_count # comments_participators comments_counter_fields(:job) diff --git a/lib/mastani_server_web/schema/cms/mutations/job.ex b/lib/mastani_server_web/schema/cms/mutations/job.ex index 0805389ea..8fd4bb776 100644 --- a/lib/mastani_server_web/schema/cms/mutations/job.ex +++ b/lib/mastani_server_web/schema/cms/mutations/job.ex @@ -15,6 +15,14 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Job do arg(:digest, non_null(:string)) arg(:length, non_null(:integer)) arg(:community_id, non_null(:id)) + + arg(:min_salary, non_null(:integer)) + arg(:max_salary, non_null(:integer)) + arg(:min_experience, non_null(:integer)) + arg(:max_experience, non_null(:integer)) + + arg(:min_education, :string) + arg(:link_addr, :string) arg(:link_source, :string) @@ -92,6 +100,12 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Job do arg(:title, :string) arg(:body, :string) arg(:digest, :string) + arg(:min_salary, :integer) + arg(:max_salary, :integer) + arg(:min_experience, :integer) + arg(:max_experience, :integer) + + arg(:min_education, :string) # ... middleware(M.Authorize, :login) diff --git a/test/mastani_server_web/mutation/cms/job_test.exs b/test/mastani_server_web/mutation/cms/job_test.exs index 714f8067f..f1bdcc062 100644 --- a/test/mastani_server_web/mutation/cms/job_test.exs +++ b/test/mastani_server_web/mutation/cms/job_test.exs @@ -26,8 +26,13 @@ defmodule MastaniServer.Test.Mutation.Job do $company: String!, $companyLogo: String! $location: String!, + $minSalary: Int!, + $maxSalary: Int!, + $minExperience: Int!, + $maxExperience: Int!, + $minEducation: String, $tags: [Ids] - ){ + ) { createJob( title: $title, body: $body, @@ -37,11 +42,19 @@ defmodule MastaniServer.Test.Mutation.Job do company: $company, companyLogo: $companyLogo, location: $location, + minSalary: $minSalary, + maxSalary: $maxSalary, + minExperience: $minExperience, + maxExperience: $maxExperience, + minEducation: $minEducation, tags: $tags ) { id title body + minSalary + maxSalary + minEducation communities { id title @@ -56,11 +69,14 @@ defmodule MastaniServer.Test.Mutation.Job do {:ok, community} = db_insert(:community) job_attr = mock_attrs(:job) - variables = job_attr |> Map.merge(%{communityId: community.id}) - variables = variables |> Map.merge(%{companyLogo: job_attr.company_logo}) + variables = job_attr |> Map.merge(%{communityId: community.id}) |> camelize_map_key created = user_conn |> mutation_result(@create_job_query, variables, "createJob") + assert created["minSalary"] == variables["minSalary"] + assert created["maxSalary"] == variables["maxSalary"] + assert created["minEducation"] == variables["minEducation"] + {:ok, found} = ORM.find(CMS.Job, created["id"]) assert created["id"] == to_string(found.id) @@ -90,11 +106,12 @@ defmodule MastaniServer.Test.Mutation.Job do end @query """ - mutation($id: ID!, $title: String, $body: String){ - updateJob(id: $id, title: $title, body: $body) { + mutation($id: ID!, $title: String, $body: String, $minSalary: Int){ + updateJob(id: $id, title: $title, body: $body, minSalary: $minSalary) { id title body + minSalary } } """ @@ -104,7 +121,8 @@ defmodule MastaniServer.Test.Mutation.Job do variables = %{ id: job.id, title: "updated title #{unique_num}", - body: "updated body #{unique_num}" + body: "updated body #{unique_num}", + minSalary: 15 } assert guest_conn |> mutation_get_error?(@query, variables, ecode(:account_login)) @@ -116,13 +134,15 @@ defmodule MastaniServer.Test.Mutation.Job do variables = %{ id: job.id, title: "updated title #{unique_num}", - body: "updated body #{unique_num}" + body: "updated body #{unique_num}", + minSalary: 15 } updated = owner_conn |> mutation_result(@query, variables, "updateJob") assert updated["title"] == variables.title assert updated["body"] == variables.body + assert updated["minSalary"] == variables.minSalary end test "login user with auth passport update a job", ~m(job)a do diff --git a/test/support/factory.ex b/test/support/factory.ex index 4ba456e31..45b29070f 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -140,6 +140,11 @@ defmodule MastaniServer.Support.Factory do length: String.length(body), author: mock(:author), views: Enum.random(0..2000), + min_salary: Enum.random(0..2000), + max_salary: Enum.random(2000..20000), + min_experience: Enum.random(1..3), + max_experience: Enum.random(5..20), + min_education: "master", communities: [ mock(:community) ] From ee761e8446bd8ece47f2b0bb47c898a0bd8ba3bf Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 25 Oct 2018 14:30:54 +0800 Subject: [PATCH 104/129] feat(job): simplify job atts, old one is complex --- lib/mastani_server/cms/job.ex | 17 +++---- .../schema/cms/cms_types.ex | 10 ++--- .../schema/cms/mutations/job.ex | 23 +++++----- ...0181024075229_add_company_link_to_jobs.exs | 9 ++++ .../20181025060156_alter_job_fields.exs | 20 +++++++++ .../mutation/cms/job_test.exs | 44 +++++++++---------- test/support/factory.ex | 10 ++--- 7 files changed, 78 insertions(+), 55 deletions(-) create mode 100644 priv/repo/migrations/20181024075229_add_company_link_to_jobs.exs create mode 100644 priv/repo/migrations/20181025060156_alter_job_fields.exs diff --git a/lib/mastani_server/cms/job.ex b/lib/mastani_server/cms/job.ex index f6df540cf..197b2b66d 100644 --- a/lib/mastani_server/cms/job.ex +++ b/lib/mastani_server/cms/job.ex @@ -17,30 +17,25 @@ defmodule MastaniServer.CMS.Job do } @required_fields ~w(title company company_logo location body digest length)a - @optional_fields ~w(link_addr link_source min_salary max_salary min_experience max_experience min_education)a + @optional_fields ~w(desc company_link link_addr salary exp education field)a @type t :: %Job{} schema "cms_jobs" do field(:title, :string) field(:company, :string) - field(:bonus, :string) field(:company_logo, :string) + field(:company_link, :string) field(:location, :string) field(:desc, :string) field(:body, :string) belongs_to(:author, Author) field(:views, :integer, default: 0) field(:link_addr, :string) - field(:link_source, :string) - field(:min_salary, :integer, default: 0) - field(:max_salary, :integer, default: 10_000_000) - - field(:min_experience, :integer, default: 1) - field(:max_experience, :integer, default: 3) - - # college - bachelor - master - doctor - field(:min_education, :string) + field(:salary, :string) + field(:exp, :string) + field(:education, :string) + field(:field, :string) field(:digest, :string) field(:length, :integer) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 118f69009..e01b016cf 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -72,6 +72,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:desc, :string) field(:company, :string) field(:company_logo, :string) + field(:company_link, :string) field(:digest, :string) field(:location, :string) field(:length, :integer) @@ -86,11 +87,10 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) - field(:min_salary, :integer) - field(:max_salary, :integer) - field(:min_experience, :integer) - field(:max_experience, :integer) - field(:min_education, :string) + field(:salary, :string) + field(:exp, :string) + field(:education, :string) + field(:field, :string) # comments_count # comments_participators diff --git a/lib/mastani_server_web/schema/cms/mutations/job.ex b/lib/mastani_server_web/schema/cms/mutations/job.ex index 8fd4bb776..84dead620 100644 --- a/lib/mastani_server_web/schema/cms/mutations/job.ex +++ b/lib/mastani_server_web/schema/cms/mutations/job.ex @@ -16,15 +16,13 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Job do arg(:length, non_null(:integer)) arg(:community_id, non_null(:id)) - arg(:min_salary, non_null(:integer)) - arg(:max_salary, non_null(:integer)) - arg(:min_experience, non_null(:integer)) - arg(:max_experience, non_null(:integer)) - - arg(:min_education, :string) + arg(:salary, non_null(:string)) + arg(:exp, non_null(:string)) + arg(:education, non_null(:string)) + arg(:field, non_null(:string)) + arg(:desc, :string) arg(:link_addr, :string) - arg(:link_source, :string) arg(:thread, :cms_thread, default_value: :job) arg(:tags, list_of(:ids)) @@ -100,12 +98,13 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Job do arg(:title, :string) arg(:body, :string) arg(:digest, :string) - arg(:min_salary, :integer) - arg(:max_salary, :integer) - arg(:min_experience, :integer) - arg(:max_experience, :integer) + arg(:salary, :string) + arg(:exp, :string) + arg(:education, :string) + arg(:field, :string) - arg(:min_education, :string) + arg(:desc, :string) + arg(:link_addr, :string) # ... middleware(M.Authorize, :login) diff --git a/priv/repo/migrations/20181024075229_add_company_link_to_jobs.exs b/priv/repo/migrations/20181024075229_add_company_link_to_jobs.exs new file mode 100644 index 000000000..7013141f8 --- /dev/null +++ b/priv/repo/migrations/20181024075229_add_company_link_to_jobs.exs @@ -0,0 +1,9 @@ +defmodule MastaniServer.Repo.Migrations.AddCompanyLinkToJobs do + use Ecto.Migration + + def change do + alter table(:cms_jobs) do + add(:company_link, :string) + end + end +end diff --git a/priv/repo/migrations/20181025060156_alter_job_fields.exs b/priv/repo/migrations/20181025060156_alter_job_fields.exs new file mode 100644 index 000000000..4cc18db71 --- /dev/null +++ b/priv/repo/migrations/20181025060156_alter_job_fields.exs @@ -0,0 +1,20 @@ +defmodule MastaniServer.Repo.Migrations.AlterJobFields do + use Ecto.Migration + + def change do + alter table(:cms_jobs) do + remove(:min_salary) + remove(:max_salary) + remove(:min_experience) + remove(:max_experience) + remove(:min_education) + remove(:link_source) + remove(:bonus) + + add(:salary, :string) + add(:exp, :string) + add(:education, :string) + add(:field, :string) + end + end +end diff --git a/test/mastani_server_web/mutation/cms/job_test.exs b/test/mastani_server_web/mutation/cms/job_test.exs index f1bdcc062..be7eaeded 100644 --- a/test/mastani_server_web/mutation/cms/job_test.exs +++ b/test/mastani_server_web/mutation/cms/job_test.exs @@ -26,11 +26,10 @@ defmodule MastaniServer.Test.Mutation.Job do $company: String!, $companyLogo: String! $location: String!, - $minSalary: Int!, - $maxSalary: Int!, - $minExperience: Int!, - $maxExperience: Int!, - $minEducation: String, + $salary: String!, + $exp: String!, + $education: String!, + $field: String!, $tags: [Ids] ) { createJob( @@ -42,19 +41,19 @@ defmodule MastaniServer.Test.Mutation.Job do company: $company, companyLogo: $companyLogo, location: $location, - minSalary: $minSalary, - maxSalary: $maxSalary, - minExperience: $minExperience, - maxExperience: $maxExperience, - minEducation: $minEducation, + salary: $salary, + exp: $exp, + education: $education, + field: $field, tags: $tags ) { id title body - minSalary - maxSalary - minEducation + salary + exp + education + field communities { id title @@ -73,9 +72,10 @@ defmodule MastaniServer.Test.Mutation.Job do created = user_conn |> mutation_result(@create_job_query, variables, "createJob") - assert created["minSalary"] == variables["minSalary"] - assert created["maxSalary"] == variables["maxSalary"] - assert created["minEducation"] == variables["minEducation"] + assert created["salary"] == variables["salary"] + assert created["exp"] == variables["exp"] + assert created["field"] == variables["field"] + assert created["education"] == variables["education"] {:ok, found} = ORM.find(CMS.Job, created["id"]) @@ -106,12 +106,12 @@ defmodule MastaniServer.Test.Mutation.Job do end @query """ - mutation($id: ID!, $title: String, $body: String, $minSalary: Int){ - updateJob(id: $id, title: $title, body: $body, minSalary: $minSalary) { + mutation($id: ID!, $title: String, $body: String, $salary: String){ + updateJob(id: $id, title: $title, body: $body, salary: $salary) { id title body - minSalary + salary } } """ @@ -122,7 +122,7 @@ defmodule MastaniServer.Test.Mutation.Job do id: job.id, title: "updated title #{unique_num}", body: "updated body #{unique_num}", - minSalary: 15 + salary: "15k-20k" } assert guest_conn |> mutation_get_error?(@query, variables, ecode(:account_login)) @@ -135,14 +135,14 @@ defmodule MastaniServer.Test.Mutation.Job do id: job.id, title: "updated title #{unique_num}", body: "updated body #{unique_num}", - minSalary: 15 + salary: "15k-20k" } updated = owner_conn |> mutation_result(@query, variables, "updateJob") assert updated["title"] == variables.title assert updated["body"] == variables.body - assert updated["minSalary"] == variables.minSalary + assert updated["salary"] == variables.salary end test "login user with auth passport update a job", ~m(job)a do diff --git a/test/support/factory.ex b/test/support/factory.ex index 45b29070f..6f89046ab 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -136,15 +136,15 @@ defmodule MastaniServer.Support.Factory do company_logo: Faker.Avatar.image_url(), location: "location #{unique_num}", body: body, + desc: "活少, 美女多", digest: String.slice(body, 1, 150), length: String.length(body), author: mock(:author), views: Enum.random(0..2000), - min_salary: Enum.random(0..2000), - max_salary: Enum.random(2000..20000), - min_experience: Enum.random(1..3), - max_experience: Enum.random(5..20), - min_education: "master", + salary: "20k-50k", + exp: "1-3年", + education: "master", + field: "互联网", communities: [ mock(:community) ] From 455edf2d3f52fd783d38730e6db3e8d65aeb71c6 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 25 Oct 2018 18:45:37 +0800 Subject: [PATCH 105/129] fix(job): missing fields --- lib/mastani_server/cms/job.ex | 4 +++- lib/mastani_server_web/schema/cms/cms_types.ex | 2 ++ lib/mastani_server_web/schema/cms/mutations/job.ex | 11 +++++++++-- .../migrations/20181025064950_add_fields_for_jobs.exs | 11 +++++++++++ test/mastani_server_web/mutation/cms/job_test.exs | 4 ++++ test/support/factory.ex | 2 ++ 6 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 priv/repo/migrations/20181025064950_add_fields_for_jobs.exs diff --git a/lib/mastani_server/cms/job.ex b/lib/mastani_server/cms/job.ex index 197b2b66d..b623d0740 100644 --- a/lib/mastani_server/cms/job.ex +++ b/lib/mastani_server/cms/job.ex @@ -17,7 +17,7 @@ defmodule MastaniServer.CMS.Job do } @required_fields ~w(title company company_logo location body digest length)a - @optional_fields ~w(desc company_link link_addr salary exp education field)a + @optional_fields ~w(desc company_link link_addr salary exp education field finance scale)a @type t :: %Job{} schema "cms_jobs" do @@ -36,6 +36,8 @@ defmodule MastaniServer.CMS.Job do field(:exp, :string) field(:education, :string) field(:field, :string) + field(:finance, :string) + field(:scale, :string) field(:digest, :string) field(:length, :integer) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index e01b016cf..b34fff00d 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -91,6 +91,8 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:exp, :string) field(:education, :string) field(:field, :string) + field(:finance, :string) + field(:scale, :string) # comments_count # comments_participators diff --git a/lib/mastani_server_web/schema/cms/mutations/job.ex b/lib/mastani_server_web/schema/cms/mutations/job.ex index 84dead620..ca6c7ccb7 100644 --- a/lib/mastani_server_web/schema/cms/mutations/job.ex +++ b/lib/mastani_server_web/schema/cms/mutations/job.ex @@ -19,10 +19,13 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Job do arg(:salary, non_null(:string)) arg(:exp, non_null(:string)) arg(:education, non_null(:string)) + arg(:finance, non_null(:string)) + arg(:scale, non_null(:string)) arg(:field, non_null(:string)) arg(:desc, :string) arg(:link_addr, :string) + arg(:copy_right, :string) arg(:thread, :cms_thread, default_value: :job) arg(:tags, list_of(:ids)) @@ -99,12 +102,16 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Job do arg(:body, :string) arg(:digest, :string) arg(:salary, :string) + arg(:copy_right, :string) + arg(:desc, :string) + arg(:link_addr, :string) + arg(:exp, :string) arg(:education, :string) arg(:field, :string) + arg(:finance, :string) + arg(:scale, :string) - arg(:desc, :string) - arg(:link_addr, :string) # ... middleware(M.Authorize, :login) diff --git a/priv/repo/migrations/20181025064950_add_fields_for_jobs.exs b/priv/repo/migrations/20181025064950_add_fields_for_jobs.exs new file mode 100644 index 000000000..d39110335 --- /dev/null +++ b/priv/repo/migrations/20181025064950_add_fields_for_jobs.exs @@ -0,0 +1,11 @@ +defmodule MastaniServer.Repo.Migrations.AddFieldsForJobs do + use Ecto.Migration + + def change do + alter table(:cms_jobs) do + add(:finance, :string) + add(:scale, :string) + add(:copy_right, :string) + end + end +end diff --git a/test/mastani_server_web/mutation/cms/job_test.exs b/test/mastani_server_web/mutation/cms/job_test.exs index be7eaeded..3be8b1ee2 100644 --- a/test/mastani_server_web/mutation/cms/job_test.exs +++ b/test/mastani_server_web/mutation/cms/job_test.exs @@ -29,6 +29,8 @@ defmodule MastaniServer.Test.Mutation.Job do $salary: String!, $exp: String!, $education: String!, + $finance: String!, + $scale: String!, $field: String!, $tags: [Ids] ) { @@ -44,6 +46,8 @@ defmodule MastaniServer.Test.Mutation.Job do salary: $salary, exp: $exp, education: $education, + finance: $finance, + scale: $scale, field: $field, tags: $tags ) { diff --git a/test/support/factory.ex b/test/support/factory.ex index 6f89046ab..28a27171c 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -145,6 +145,8 @@ defmodule MastaniServer.Support.Factory do exp: "1-3年", education: "master", field: "互联网", + finance: "x轮", + scale: "300人", communities: [ mock(:community) ] From 4f29efa8040528d5941ea62ce51326562305c7c5 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 25 Oct 2018 18:47:41 +0800 Subject: [PATCH 106/129] fix(graphql parse error): fix graphql error crash server caused by jason encoder config in router --- lib/mastani_server_web/router.ex | 2 +- test/mastani_server_web/mutation/cms/post_test.exs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/mastani_server_web/router.ex b/lib/mastani_server_web/router.ex index 3a8d69c32..598c1143c 100644 --- a/lib/mastani_server_web/router.ex +++ b/lib/mastani_server_web/router.ex @@ -16,7 +16,7 @@ defmodule MastaniServerWeb.Router do "/", Absinthe.Plug.GraphiQL, schema: MastaniServerWeb.Schema, - json_codec: Jason, + # json_codec: Jason, pipeline: {ApolloTracing.Pipeline, :plug}, interface: :playground, context: %{pubsub: MastaniServerWeb.Endpoint} diff --git a/test/mastani_server_web/mutation/cms/post_test.exs b/test/mastani_server_web/mutation/cms/post_test.exs index b9ed8210e..be8c30724 100644 --- a/test/mastani_server_web/mutation/cms/post_test.exs +++ b/test/mastani_server_web/mutation/cms/post_test.exs @@ -39,6 +39,19 @@ defmodule MastaniServer.Test.Mutation.Post do assert {:ok, _} = ORM.find_by(CMS.Author, user_id: user.id) end + # NOTE: this test is IMPORTANT, cause json_codec: Jason in router will cause + # server crash when GraphQL parse error + test "create post with missing non_null field should get 200 error" do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + {:ok, community} = db_insert(:community) + post_attr = mock_attrs(:post) + variables = post_attr |> Map.merge(%{communityId: community.id}) |> Map.delete(:title) + + assert user_conn |> mutation_get_error?(@create_post_query, variables) + end + test "can create post with tags" do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) From 3be43dad2ecb80305645404e5ab4ec4e76635b6d Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 25 Oct 2018 21:45:54 +0800 Subject: [PATCH 107/129] fix(job create): clean up & refactor --- lib/mastani_server/cms/job.ex | 6 +++--- lib/mastani_server_web/schema/cms/cms_types.ex | 2 +- lib/mastani_server_web/schema/cms/mutations/job.ex | 2 +- .../migrations/20181025130936_cleaup_for_jobs.exs | 12 ++++++++++++ .../20181025131259_copy_right_default_for_jobs.exs | 11 +++++++++++ test/mastani_server_web/mutation/cms/job_test.exs | 2 -- test/support/factory.ex | 1 - 7 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 priv/repo/migrations/20181025130936_cleaup_for_jobs.exs create mode 100644 priv/repo/migrations/20181025131259_copy_right_default_for_jobs.exs diff --git a/lib/mastani_server/cms/job.ex b/lib/mastani_server/cms/job.ex index b623d0740..117d312a8 100644 --- a/lib/mastani_server/cms/job.ex +++ b/lib/mastani_server/cms/job.ex @@ -16,8 +16,8 @@ defmodule MastaniServer.CMS.Job do Tag } - @required_fields ~w(title company company_logo location body digest length)a - @optional_fields ~w(desc company_link link_addr salary exp education field finance scale)a + @required_fields ~w(title company company_logo body digest length)a + @optional_fields ~w(desc company_link link_addr copy_right salary exp education field finance scale)a @type t :: %Job{} schema "cms_jobs" do @@ -25,12 +25,12 @@ defmodule MastaniServer.CMS.Job do field(:company, :string) field(:company_logo, :string) field(:company_link, :string) - field(:location, :string) field(:desc, :string) field(:body, :string) belongs_to(:author, Author) field(:views, :integer, default: 0) field(:link_addr, :string) + field(:copy_right, :string) field(:salary, :string) field(:exp, :string) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index b34fff00d..836c0675d 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -74,9 +74,9 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:company_logo, :string) field(:company_link, :string) field(:digest, :string) - field(:location, :string) field(:length, :integer) field(:link_addr, :string) + field(:copy_right, :string) field(:body, :string) field(:views, :integer) diff --git a/lib/mastani_server_web/schema/cms/mutations/job.ex b/lib/mastani_server_web/schema/cms/mutations/job.ex index ca6c7ccb7..79d3e2602 100644 --- a/lib/mastani_server_web/schema/cms/mutations/job.ex +++ b/lib/mastani_server_web/schema/cms/mutations/job.ex @@ -10,7 +10,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Job do arg(:title, non_null(:string)) arg(:company, non_null(:string)) arg(:company_logo, non_null(:string)) - arg(:location, non_null(:string)) + arg(:company_link, :string) arg(:body, non_null(:string)) arg(:digest, non_null(:string)) arg(:length, non_null(:integer)) diff --git a/priv/repo/migrations/20181025130936_cleaup_for_jobs.exs b/priv/repo/migrations/20181025130936_cleaup_for_jobs.exs new file mode 100644 index 000000000..46c8dcefb --- /dev/null +++ b/priv/repo/migrations/20181025130936_cleaup_for_jobs.exs @@ -0,0 +1,12 @@ +defmodule MastaniServer.Repo.Migrations.CleaupForJobs do + use Ecto.Migration + + def change do + alter table(:cms_jobs) do + remove(:location) + remove(:copy_right) + + add(:copy_right, :string, default_value: "original") + end + end +end diff --git a/priv/repo/migrations/20181025131259_copy_right_default_for_jobs.exs b/priv/repo/migrations/20181025131259_copy_right_default_for_jobs.exs new file mode 100644 index 000000000..703425b0b --- /dev/null +++ b/priv/repo/migrations/20181025131259_copy_right_default_for_jobs.exs @@ -0,0 +1,11 @@ +defmodule MastaniServer.Repo.Migrations.CopyRightDefaultForJobs do + use Ecto.Migration + + def change do + alter table(:cms_jobs) do + remove(:copy_right) + + add(:copy_right, :string, default: "original") + end + end +end diff --git a/test/mastani_server_web/mutation/cms/job_test.exs b/test/mastani_server_web/mutation/cms/job_test.exs index 3be8b1ee2..f44a45b30 100644 --- a/test/mastani_server_web/mutation/cms/job_test.exs +++ b/test/mastani_server_web/mutation/cms/job_test.exs @@ -25,7 +25,6 @@ defmodule MastaniServer.Test.Mutation.Job do $communityId: ID!, $company: String!, $companyLogo: String! - $location: String!, $salary: String!, $exp: String!, $education: String!, @@ -42,7 +41,6 @@ defmodule MastaniServer.Test.Mutation.Job do communityId: $communityId, company: $company, companyLogo: $companyLogo, - location: $location, salary: $salary, exp: $exp, education: $education, diff --git a/test/support/factory.ex b/test/support/factory.ex index 28a27171c..1d35d9f97 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -134,7 +134,6 @@ defmodule MastaniServer.Support.Factory do title: Faker.Lorem.Shakespeare.king_richard_iii(), company: Faker.Company.name(), company_logo: Faker.Avatar.image_url(), - location: "location #{unique_num}", body: body, desc: "活少, 美女多", digest: String.slice(body, 1, 150), From 8e55f3072b4188546c004826a22508dbdf608774 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 28 Oct 2018 11:52:02 +0800 Subject: [PATCH 108/129] feat(topic): basic topic concept --- lib/helper/query_builder.ex | 7 +++ lib/mastani_server/cms/cms.ex | 1 + .../cms/delegates/article_curd.ex | 18 ++++++- .../cms/delegates/article_operation.ex | 23 ++++++++- lib/mastani_server/cms/post.ex | 13 ++++- lib/mastani_server/cms/topic.ex | 49 +++++++++++++++++++ lib/mastani_server_web/schema/cms/cms_misc.ex | 7 +++ .../schema/cms/mutations/post.ex | 1 + ...181028010610_create_topic_for_contents.exs | 17 +++++++ .../20181028015325_create_topic_post_join.exs | 12 +++++ 10 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 lib/mastani_server/cms/topic.ex create mode 100644 priv/repo/migrations/20181028010610_create_topic_for_contents.exs create mode 100644 priv/repo/migrations/20181028015325_create_topic_post_join.exs diff --git a/lib/helper/query_builder.ex b/lib/helper/query_builder.ex index 6a3d1cfe0..9682baa3d 100644 --- a/lib/helper/query_builder.ex +++ b/lib/helper/query_builder.ex @@ -185,6 +185,13 @@ defmodule Helper.QueryBuilder do where: t.title == ^tag_name ) + {:topic, topic}, queryable -> + from( + q in queryable, + join: t in assoc(q, :topics), + where: t.raw == ^topic + ) + {:category, catetory_raw}, queryable -> from( q in queryable, diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index 28741b0ab..798e5940a 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -63,6 +63,7 @@ defmodule MastaniServer.CMS do defdelegate read_content(thread, id, user), to: ArticleCURD defdelegate paged_contents(queryable, filter), to: ArticleCURD defdelegate create_content(community, thread, attrs, user), to: ArticleCURD + defdelegate create_content(community, thread, attrs, user, topic), to: ArticleCURD defdelegate reaction_users(thread, react, id, filters), to: ArticleCURD # ArticleReaction diff --git a/lib/mastani_server/cms/delegates/article_curd.ex b/lib/mastani_server/cms/delegates/article_curd.ex index a143b2dbd..8a7792f98 100644 --- a/lib/mastani_server/cms/delegates/article_curd.ex +++ b/lib/mastani_server/cms/delegates/article_curd.ex @@ -16,7 +16,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do alias CMS.Delegate.ArticleOperation alias Helper.{ORM, QueryBuilder} - alias CMS.{Author, Community, Tag} + alias CMS.{Author, Community, Tag, Topic} alias Ecto.Multi @doc """ @@ -118,7 +118,13 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do {:error, %Ecto.Changeset{}} """ - def create_content(%Community{id: community_id}, thread, attrs, %User{id: user_id}) do + def create_content( + %Community{id: community_id}, + thread, + attrs, + %User{id: user_id}, + topic \\ "index" + ) do with {:ok, author} <- ensure_author_exists(%User{id: user_id}), {:ok, action} <- match_action(thread, :community), {:ok, community} <- ORM.find(Community, community_id) do @@ -133,6 +139,9 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do |> Multi.run(:set_community, fn %{add_content_author: content} -> ArticleOperation.set_community(community, thread, content.id) end) + |> Multi.run(:set_topic, fn %{add_content_author: content} -> + ArticleOperation.set_topic(%Topic{title: topic}, thread, content.id) + end) |> Multi.run(:set_community_flag, fn %{add_content_author: content} -> # TODO: remove this judge, as content should have a flag case action |> Map.has_key?(:flag) do @@ -175,6 +184,11 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do {:error, [message: "set community flag", code: ecode(:create_fails)]} end + defp create_content_result({:error, :set_topic, result, _steps}) do + IO.inspect(result, label: "set topic") + {:error, [message: "set topic", code: ecode(:create_fails)]} + end + defp create_content_result({:error, :set_tag, result, _steps}) do {:error, result} end diff --git a/lib/mastani_server/cms/delegates/article_operation.ex b/lib/mastani_server/cms/delegates/article_operation.ex index 2268cf151..b71a3e40d 100644 --- a/lib/mastani_server/cms/delegates/article_operation.ex +++ b/lib/mastani_server/cms/delegates/article_operation.ex @@ -18,7 +18,8 @@ defmodule MastaniServer.CMS.Delegate.ArticleOperation do RepoCommunityFlag, Video, VideoCommunityFlag, - Tag + Tag, + Topic } alias MastaniServer.CMS.Repo, as: CMSRepo @@ -131,6 +132,26 @@ defmodule MastaniServer.CMS.Delegate.ArticleOperation do end end + @doc """ + set topic only for post + """ + def set_topic(%Topic{title: title}, :post, content_id) do + with {:ok, content} <- ORM.find(Post, content_id, preload: :topics), + {:ok, topic} <- + ORM.findby_or_insert(Topic, %{title: title}, %{ + title: title, + thread: "post", + raw: title + }) do + content + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_assoc(:topics, content.topics ++ [topic]) + |> Repo.update() + end + end + + def set_topic(_topic, _thread, _content_id), do: {:ok, :pass} + # make sure the reuest tag is in the current community thread # example: you can't set a other thread tag to this thread's article defp tag_in_community_thread?(%Community{id: communityId}, thread, tag) do diff --git a/lib/mastani_server/cms/post.ex b/lib/mastani_server/cms/post.ex index a47d6eab3..120500778 100644 --- a/lib/mastani_server/cms/post.ex +++ b/lib/mastani_server/cms/post.ex @@ -13,7 +13,8 @@ defmodule MastaniServer.CMS.Post do PostFavorite, PostStar, PostViewer, - Tag + Tag, + Topic } @required_fields ~w(title body digest length)a @@ -56,6 +57,16 @@ defmodule MastaniServer.CMS.Post do on_replace: :delete ) + many_to_many( + :topics, + Topic, + join_through: "posts_topics", + join_keys: [post_id: :id, topic_id: :id], + # :delete_all will only remove data from the join source + on_delete: :delete_all, + on_replace: :delete + ) + many_to_many( :communities, Community, diff --git a/lib/mastani_server/cms/topic.ex b/lib/mastani_server/cms/topic.ex new file mode 100644 index 000000000..4050eb7b0 --- /dev/null +++ b/lib/mastani_server/cms/topic.ex @@ -0,0 +1,49 @@ +defmodule MastaniServer.CMS.Topic do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + # alias MastaniServer.CMS.{Author, Community, Job, Post, Video} + + @required_fields ~w(thread title raw)a + + @type t :: %Topic{} + schema "topics" do + field(:title, :string) + field(:thread, :string) + field(:raw, :string) + + # many_to_many( + # :posts, + # Post, + # join_through: "posts_tags", + # join_keys: [post_id: :id, tag_id: :id], + # on_delete: :delete_all + # ) + + # many_to_many( + # :videos, + # Video, + # join_through: "videos_tags", + # join_keys: [video_id: :id, tag_id: :id] + # ) + + # many_to_many( + # :jobs, + # Job, + # join_through: "jobs_tags", + # join_keys: [job_id: :id, tag_id: :id] + # ) + + timestamps(type: :utc_datetime) + end + + def changeset(%Topic{} = topic, attrs) do + topic + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + + # |> unique_constraint(:tag_duplicate, name: :tags_community_id_thread_title_index) + end +end diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index d72f1abf9..24b6e5d6e 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -70,6 +70,11 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do value(:cheatsheet) end + enum :cms_topic do + value(:index) + value(:city) + end + enum :order_enum do value(:asc) value(:desc) @@ -174,6 +179,8 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do field(:sort, :sort_enum) field(:tag, :string, default_value: :all) field(:community, :string) + # field(:topic, :string, default_value: :index) + field(:topic, :string) # @desc "Matching a name" # field(:order, :order_enum, default_value: :desc) diff --git a/lib/mastani_server_web/schema/cms/mutations/post.ex b/lib/mastani_server_web/schema/cms/mutations/post.ex index 7d101ac7d..a8498758f 100644 --- a/lib/mastani_server_web/schema/cms/mutations/post.ex +++ b/lib/mastani_server_web/schema/cms/mutations/post.ex @@ -15,6 +15,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Post do arg(:copy_right, :string) arg(:community_id, non_null(:id)) arg(:thread, :cms_thread, default_value: :post) + arg(:topic, :cms_topic, default_value: :index) arg(:tags, list_of(:ids)) middleware(M.Authorize, :login) diff --git a/priv/repo/migrations/20181028010610_create_topic_for_contents.exs b/priv/repo/migrations/20181028010610_create_topic_for_contents.exs new file mode 100644 index 000000000..03b103d22 --- /dev/null +++ b/priv/repo/migrations/20181028010610_create_topic_for_contents.exs @@ -0,0 +1,17 @@ +defmodule MastaniServer.Repo.Migrations.CreateTopicForContents do + use Ecto.Migration + + def change do + create table(:topics) do + # add(:community_id, references(:communities, on_delete: :delete_all), null: false) + # add(:user_id, references(:users)) + add(:thread, :string) + add(:title, :string, default: "index") + add(:raw, :string, default: "index") + + timestamps() + end + + # create(unique_index(:topics, [:community, :part, :title])) + end +end diff --git a/priv/repo/migrations/20181028015325_create_topic_post_join.exs b/priv/repo/migrations/20181028015325_create_topic_post_join.exs new file mode 100644 index 000000000..363e607db --- /dev/null +++ b/priv/repo/migrations/20181028015325_create_topic_post_join.exs @@ -0,0 +1,12 @@ +defmodule MastaniServer.Repo.Migrations.CreateTopicPostJoin do + use Ecto.Migration + + def change do + create table(:posts_topics) do + add(:topic_id, references(:topics, on_delete: :delete_all), null: false) + add(:post_id, references(:cms_posts, on_delete: :delete_all), null: false) + end + + create(unique_index(:posts_topics, [:topic_id, :post_id])) + end +end From 17474943b8c3f9ee3a1900e64c51600ad4cc40f9 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 28 Oct 2018 12:56:08 +0800 Subject: [PATCH 109/129] refactor(topic on posts): impl topic & test only on posts --- lib/mastani_server/cms/cms.ex | 2 +- .../cms/delegates/article_curd.ex | 4 +- lib/mastani_server_web/schema/cms/cms_misc.ex | 16 +++- .../schema/cms/cms_queries.ex | 3 +- .../schema/cms/cms_types.ex | 1 + .../query/cms/posts_topic_test.exs | 74 +++++++++++++++++++ 6 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 test/mastani_server_web/query/cms/posts_topic_test.exs diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index 798e5940a..ef8a83be6 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -63,7 +63,7 @@ defmodule MastaniServer.CMS do defdelegate read_content(thread, id, user), to: ArticleCURD defdelegate paged_contents(queryable, filter), to: ArticleCURD defdelegate create_content(community, thread, attrs, user), to: ArticleCURD - defdelegate create_content(community, thread, attrs, user, topic), to: ArticleCURD + defdelegate create_content(community, thread, attrs, user, options), to: ArticleCURD defdelegate reaction_users(thread, react, id, filters), to: ArticleCURD # ArticleReaction diff --git a/lib/mastani_server/cms/delegates/article_curd.ex b/lib/mastani_server/cms/delegates/article_curd.ex index 8a7792f98..a77508e1a 100644 --- a/lib/mastani_server/cms/delegates/article_curd.ex +++ b/lib/mastani_server/cms/delegates/article_curd.ex @@ -123,7 +123,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do thread, attrs, %User{id: user_id}, - topic \\ "index" + options \\ %{topic: "index"} ) do with {:ok, author} <- ensure_author_exists(%User{id: user_id}), {:ok, action} <- match_action(thread, :community), @@ -140,7 +140,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do ArticleOperation.set_community(community, thread, content.id) end) |> Multi.run(:set_topic, fn %{add_content_author: content} -> - ArticleOperation.set_topic(%Topic{title: topic}, thread, content.id) + ArticleOperation.set_topic(%Topic{title: options.topic}, thread, content.id) end) |> Multi.run(:set_community_flag, fn %{add_content_author: content} -> # TODO: remove this judge, as content should have a flag diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index 24b6e5d6e..f1d557761 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -179,9 +179,6 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do field(:sort, :sort_enum) field(:tag, :string, default_value: :all) field(:community, :string) - # field(:topic, :string, default_value: :index) - field(:topic, :string) - # @desc "Matching a name" # field(:order, :order_enum, default_value: :desc) @@ -189,6 +186,19 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do # field(:tag, :string, default_value: :all) end + @desc "posts_filter doc" + input_object :paged_posts_filter do + @desc "limit of records (default 20), if first > 30, only return 30 at most" + pagination_args() + + field(:when, :when_enum) + field(:sort, :sort_enum) + field(:tag, :string, default_value: :all) + field(:community, :string) + # field(:topic, :string, default_value: "index") + field(:topic, :string) + end + @doc """ cms github repo contribotor """ diff --git a/lib/mastani_server_web/schema/cms/cms_queries.ex b/lib/mastani_server_web/schema/cms/cms_queries.ex index 179cc02b5..36cbcd37e 100644 --- a/lib/mastani_server_web/schema/cms/cms_queries.ex +++ b/lib/mastani_server_web/schema/cms/cms_queries.ex @@ -72,7 +72,8 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do @desc "get paged posts" field :paged_posts, :paged_posts do - arg(:filter, non_null(:paged_article_filter)) + arg(:filter, non_null(:paged_posts_filter)) + # arg(:filter, non_null(:paged_article_filter)) middleware(M.PageSizeProof) resolve(&R.CMS.paged_posts/3) diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 836c0675d..156d25831 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -34,6 +34,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:author, :user, resolve: dataloader(CMS, :author)) field(:communities, list_of(:community), resolve: dataloader(CMS, :communities)) + # field(:topic) field :comments, list_of(:comment) do arg(:filter, :members_filter) diff --git a/test/mastani_server_web/query/cms/posts_topic_test.exs b/test/mastani_server_web/query/cms/posts_topic_test.exs new file mode 100644 index 000000000..497423fbd --- /dev/null +++ b/test/mastani_server_web/query/cms/posts_topic_test.exs @@ -0,0 +1,74 @@ +defmodule MastaniServer.Test.Query.PostsTopic do + use MastaniServer.TestTools + + # import Helper.Utils, only: [get_config: 2] + alias MastaniServer.CMS + + setup do + {:ok, user} = db_insert(:user) + {:ok, community} = db_insert(:community) + post_attrs = mock_attrs(:post, %{community_id: community.id}) + {:ok, _post} = CMS.create_content(community, :post, post_attrs, user, %{topic: "index"}) + post_attrs = mock_attrs(:post, %{community_id: community.id}) + {:ok, _post} = CMS.create_content(community, :post, post_attrs, user, %{topic: "index"}) + + guest_conn = simu_conn(:guest) + + {:ok, ~m(guest_conn user community)a} + end + + describe "[query posts topic filter]" do + @query """ + query($filter: PagedPostsFilter!) { + pagedPosts(filter: $filter) { + entries { + id + title + } + totalCount + } + } + """ + test "topic filter on posts should work", ~m(guest_conn)a do + variables = %{filter: %{page: 1, size: 10}} + results = guest_conn |> query_result(@query, variables, "pagedPosts") + assert results["totalCount"] == 2 + + variables = %{filter: %{page: 1, size: 10, topic: "index"}} + results = guest_conn |> query_result(@query, variables, "pagedPosts") + assert results["totalCount"] == 2 + + variables = %{filter: %{page: 1, size: 10, topic: "city"}} + results = guest_conn |> query_result(@query, variables, "pagedPosts") + assert results["totalCount"] == 0 + end + end + + describe "[query non-posts topic filter]" do + @query """ + query($filter: PagedArticleFilter!) { + pagedJobs(filter: $filter) { + entries { + id + title + } + totalCount + } + } + """ + test "topic filter on non-posts has no effect", ~m(guest_conn user community)a do + job_attrs = mock_attrs(:job, %{community_id: community.id}) + {:ok, _} = CMS.create_content(community, :job, job_attrs, user, %{topic: "index"}) + + job_attrs = mock_attrs(:job, %{community_id: community.id}) + {:ok, _} = CMS.create_content(community, :job, job_attrs, user, %{topic: "city"}) + + variables = %{filter: %{community: community.raw, page: 1, size: 10}} + results = guest_conn |> query_result(@query, variables, "pagedJobs") + assert results["totalCount"] == 2 + + variables = %{filter: %{page: 1, size: 10, topic: "index"}} + assert guest_conn |> query_get_error?(@query, variables) + end + end +end From 00242696e62a81a91a94b5860f5eca8d940133b0 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 28 Oct 2018 23:26:32 +0800 Subject: [PATCH 110/129] feat(tags): add topic support --- lib/helper/utils.ex | 2 +- lib/mastani_server/cms/cms.ex | 1 + .../cms/delegates/community_curd.ex | 58 ++++++++++++++++--- lib/mastani_server/cms/tag.ex | 8 ++- .../resolvers/cms_resolver.ex | 8 +++ .../schema/cms/cms_queries.ex | 1 + .../schema/cms/mutations/community.ex | 1 + .../20181028050903_add_topic_to_tags.exs | 13 +++++ test/mastani_server/cms/cms_test.exs | 1 + .../mutation/cms/cms_test.exs | 9 ++- .../mastani_server_web/query/cms/cms_test.exs | 27 ++++++--- 11 files changed, 107 insertions(+), 22 deletions(-) create mode 100644 priv/repo/migrations/20181028050903_add_topic_to_tags.exs diff --git a/lib/helper/utils.ex b/lib/helper/utils.ex index efa9649e5..48652e8fb 100644 --- a/lib/helper/utils.ex +++ b/lib/helper/utils.ex @@ -122,7 +122,7 @@ defmodule Helper.Utils do results = Enum.map(attrs, fn {k, v} -> if is_atom(v) do - {k, to_string(v)} + {k, v |> to_string() |> String.upcase()} else {k, v} end diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index ef8a83be6..20ee4b61f 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -36,6 +36,7 @@ defmodule MastaniServer.CMS do defdelegate create_tag(thread, attrs, user), to: CommunityCURD defdelegate update_tag(attrs), to: CommunityCURD defdelegate get_tags(community, thread), to: CommunityCURD + defdelegate get_tags(community, thread, topic), to: CommunityCURD defdelegate get_tags(filter), to: CommunityCURD # >> wiki & cheatsheet (sync with github) defdelegate get_wiki(community), to: CommunitySync diff --git a/lib/mastani_server/cms/delegates/community_curd.ex b/lib/mastani_server/cms/delegates/community_curd.ex index c0425290d..6a0f23d49 100644 --- a/lib/mastani_server/cms/delegates/community_curd.ex +++ b/lib/mastani_server/cms/delegates/community_curd.ex @@ -18,6 +18,7 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do CommunityEditor, CommunitySubscriber, Tag, + Topic, Thread } @@ -51,27 +52,47 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do @doc """ create a Tag base on type: post / tuts / videos ... """ - def create_tag(thread, attrs, %Accounts.User{id: user_id}) when valid_thread(thread) do + def create_tag(thread, attrs, %Accounts.User{id: user_id}) do with {:ok, action} <- match_action(thread, :tag), {:ok, author} <- ensure_author_exists(%Accounts.User{id: user_id}), - {:ok, _community} <- ORM.find(Community, attrs.community_id) do - attrs = attrs |> Map.merge(%{author_id: author.id}) - attrs = attrs |> map_atom_value(:string) + {:ok, _community} <- ORM.find(Community, attrs.community_id), + {:ok, topic} = find_or_insert_topic(attrs) do + attrs = + attrs + |> Map.merge(%{author_id: author.id, topic_id: topic.id}) + |> map_atom_value(:string) action.reactor |> ORM.create(attrs) end end def update_tag(%{id: _id} = attrs) do - attrs = attrs |> map_atom_value(:string) - Tag |> ORM.find_update(%{id: attrs.id, title: attrs.title, color: attrs.color}) + ~m(id title color)a = attrs |> map_atom_value(:string) + + with {:ok, %{id: topic_id}} = find_or_insert_topic(attrs) do + Tag + |> ORM.find_update(~m(id title color color topic_id)a) + end end @doc """ get tags belongs to a community / thread """ + def get_tags(%Community{id: community_id}, thread, topic) when not is_nil(community_id) do + thread = thread |> to_string |> String.upcase() + topic = topic |> to_string |> String.upcase() + + Tag + |> join(:inner, [t], c in assoc(t, :community)) + |> join(:inner, [t], cp in assoc(t, :topic)) + |> where([t, c, cp], c.id == ^community_id and t.thread == ^thread and cp.title == ^topic) + |> distinct([t], t.title) + |> Repo.all() + |> done() + end + def get_tags(%Community{id: community_id}, thread) when not is_nil(community_id) do - thread = to_string(thread) + thread = thread |> to_string |> String.upcase() Tag |> join(:inner, [t], c in assoc(t, :community)) @@ -81,8 +102,8 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do |> done() end - def get_tags(%Community{raw: community_raw}, thread) when not is_nil(community_raw) do - thread = to_string(thread) + def get_tags(%Community{raw: community_raw}, thread) do + thread = thread |> to_string |> String.upcase() Tag |> join(:inner, [t], c in assoc(t, :community)) @@ -141,4 +162,23 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do {:ok, geo_info_data} end end + + defp find_or_insert_topic(%{topic: title} = attrs) when is_atom(title) do + title = title |> to_string() |> String.upcase() + thread = attrs.thread |> to_string() |> String.upcase() + + ORM.findby_or_insert(Topic, %{title: title}, %{ + title: title, + thread: thread, + raw: title + }) + end + + defp find_or_insert_topic(%{thread: thread}) do + find_or_insert_topic(%{topic: :index, thread: thread}) + end + + defp find_or_insert_topic(_attrs) do + find_or_insert_topic(%{topic: :index, thread: :post}) + end end diff --git a/lib/mastani_server/cms/tag.ex b/lib/mastani_server/cms/tag.ex index 2e0dcb64a..295a8ec52 100644 --- a/lib/mastani_server/cms/tag.ex +++ b/lib/mastani_server/cms/tag.ex @@ -4,9 +4,10 @@ defmodule MastaniServer.CMS.Tag do use Ecto.Schema import Ecto.Changeset - alias MastaniServer.CMS.{Author, Community, Job, Post, Video} + alias MastaniServer.CMS.{Author, Community, Topic, Job, Post, Video} - @required_fields ~w(thread title color author_id community_id)a + @required_fields ~w(thread title color author_id topic_id community_id)a + # @required_fields ~w(thread title color author_id community_id)a @type t :: %Tag{} schema "tags" do @@ -14,6 +15,7 @@ defmodule MastaniServer.CMS.Tag do field(:color, :string) field(:thread, :string) belongs_to(:community, Community) + belongs_to(:topic, Topic) belongs_to(:author, Author) many_to_many( @@ -49,7 +51,7 @@ defmodule MastaniServer.CMS.Tag do |> validate_required(@required_fields) |> foreign_key_constraint(:user_id) |> foreign_key_constraint(:community_id) - |> unique_constraint(:tag_duplicate, name: :tags_community_id_thread_title_index) + |> unique_constraint(:tag_duplicate, name: :tags_community_id_thread_topic_id_title_index) # |> foreign_key_constraint(name: :posts_tags_tag_id_fkey) end diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index 5066226d6..ec1935219 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -199,6 +199,14 @@ defmodule MastaniServerWeb.Resolvers.CMS do def unset_tag(_root, ~m(id thread tag_id)a, _info), do: CMS.unset_tag(thread, %Tag{id: tag_id}, id) + def get_tags(_root, ~m(community_id thread topic)a, _info) do + CMS.get_tags(%Community{id: community_id}, thread, topic) + end + + def get_tags(_root, ~m(community thread topic)a, _info) do + CMS.get_tags(%Community{raw: community}, thread, topic) + end + def get_tags(_root, ~m(community_id thread)a, _info) do CMS.get_tags(%Community{id: community_id}, thread) end diff --git a/lib/mastani_server_web/schema/cms/cms_queries.ex b/lib/mastani_server_web/schema/cms/cms_queries.ex index 36cbcd37e..6a052185f 100644 --- a/lib/mastani_server_web/schema/cms/cms_queries.ex +++ b/lib/mastani_server_web/schema/cms/cms_queries.ex @@ -168,6 +168,7 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do arg(:community_id, :id) arg(:community, :string) arg(:thread, :cms_thread, default_value: :post) + arg(:topic, :cms_topic) resolve(&R.CMS.get_tags/3) end diff --git a/lib/mastani_server_web/schema/cms/mutations/community.ex b/lib/mastani_server_web/schema/cms/mutations/community.ex index 891c1e035..cb999c3e7 100644 --- a/lib/mastani_server_web/schema/cms/mutations/community.ex +++ b/lib/mastani_server_web/schema/cms/mutations/community.ex @@ -134,6 +134,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Community do arg(:color, non_null(:rainbow_color_enum)) arg(:community_id, non_null(:id)) arg(:thread, :cms_thread, default_value: :post) + arg(:topic, :cms_topic, default_value: :index) middleware(M.Authorize, :login) middleware(M.PassportLoader, source: :community) diff --git a/priv/repo/migrations/20181028050903_add_topic_to_tags.exs b/priv/repo/migrations/20181028050903_add_topic_to_tags.exs new file mode 100644 index 000000000..645f26348 --- /dev/null +++ b/priv/repo/migrations/20181028050903_add_topic_to_tags.exs @@ -0,0 +1,13 @@ +defmodule MastaniServer.Repo.Migrations.AddTopicToTags do + use Ecto.Migration + + def change do + drop(unique_index(:tags, [:community_id, :thread, :title])) + + alter table(:tags) do + add(:topic_id, references(:topics, on_delete: :delete_all)) + end + + create(unique_index(:tags, [:community_id, :thread, :topic_id, :title])) + end +end diff --git a/test/mastani_server/cms/cms_test.exs b/test/mastani_server/cms/cms_test.exs index 6880f93e5..f24363627 100644 --- a/test/mastani_server/cms/cms_test.exs +++ b/test/mastani_server/cms/cms_test.exs @@ -15,6 +15,7 @@ defmodule MastaniServer.Test.CMS do end describe "[cms tag]" do + @tag :wip2 test "create tag with valid data", ~m(community user)a do valid_attrs = mock_attrs(:tag, %{community_id: community.id}) diff --git a/test/mastani_server_web/mutation/cms/cms_test.exs b/test/mastani_server_web/mutation/cms/cms_test.exs index f675656cb..3517d9d7d 100644 --- a/test/mastani_server_web/mutation/cms/cms_test.exs +++ b/test/mastani_server_web/mutation/cms/cms_test.exs @@ -202,22 +202,25 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do } } """ + @tag :wip2 test "create tag with valid attrs, has default POST thread", ~m(community)a do variables = mock_attrs(:tag, %{communityId: community.id}) passport_rules = %{community.title => %{"post.tag.create" => true}} rule_conn = simu_conn(:user, cms: passport_rules) + # IO.inspect variables, label: "hello variables" created = rule_conn |> mutation_result(@create_tag_query, variables, "createTag") belong_community = created["community"] {:ok, found} = Tag |> ORM.find(created["id"]) assert created["id"] == to_string(found.id) - assert found.thread == "post" + assert found.thread == "POST" assert belong_community["id"] == to_string(community.id) end + @tag :wip2 test "auth user create duplicate tag fails", ~m(community)a do variables = mock_attrs(:tag, %{communityId: community.id}) passport_rules = %{community.title => %{"post.tag.create" => true}} @@ -264,6 +267,7 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do } } """ + @tag :wip test "auth user can update a tag", ~m(tag community)a do variables = %{id: tag.id, color: "GREEN", title: "new title", communityId: community.id} @@ -271,7 +275,8 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do rule_conn = simu_conn(:user, cms: passport_rules) updated = rule_conn |> mutation_result(@update_tag_query, variables, "updateTag") - assert updated["color"] == "green" + + assert updated["color"] == "GREEN" assert updated["title"] == "new title" end diff --git a/test/mastani_server_web/query/cms/cms_test.exs b/test/mastani_server_web/query/cms/cms_test.exs index 667200683..7c727178d 100644 --- a/test/mastani_server_web/query/cms/cms_test.exs +++ b/test/mastani_server_web/query/cms/cms_test.exs @@ -246,8 +246,8 @@ defmodule MastaniServer.Test.Query.CMS.Basic do end @query """ - query($communityId: ID, $thread: CmsThread!) { - partialTags(communityId: $communityId, thread: $thread) { + query($communityId: ID, $thread: CmsThread!, $topic: CmsTopic ) { + partialTags(communityId: $communityId, thread: $thread, topic: $topic) { id title color @@ -260,9 +260,10 @@ defmodule MastaniServer.Test.Query.CMS.Basic do } } """ - test "guest user can get partial tags by communityId", ~m(guest_conn community)a do - {:ok, tag} = db_insert(:tag, %{thread: "post", community: community}) - {:ok, tag2} = db_insert(:tag, %{thread: "job", community: community}) + @tag :wip2 + test "guest user can get partial tags by communityId and thread", ~m(guest_conn community)a do + {:ok, tag} = db_insert(:tag, %{thread: "POST", community: community}) + {:ok, tag2} = db_insert(:tag, %{thread: "JOB", community: community}) variables = %{thread: "POST", communityId: community.id} @@ -272,6 +273,17 @@ defmodule MastaniServer.Test.Query.CMS.Basic do assert results |> Enum.any?(&(&1["id"] != to_string(tag2.id))) end + @tag :wip2 + test "user can get partial tags by topic", ~m(guest_conn community user)a do + valid_attrs = mock_attrs(:tag, %{community_id: community.id}) + {:ok, _tag} = CMS.create_tag(:post, valid_attrs, %User{id: user.id}) + + variables = %{thread: "POST", communityId: community.id, topic: "INDEX"} + + results = guest_conn |> query_result(@query, variables, "partialTags") + assert results |> length == 1 + end + @query """ query($community: String, $thread: CmsThread!) { partialTags(community: $community, thread: $thread) { @@ -287,9 +299,10 @@ defmodule MastaniServer.Test.Query.CMS.Basic do } } """ + @tag :wip2 test "guest user can get partial tags by communityRaw", ~m(guest_conn community)a do - {:ok, tag} = db_insert(:tag, %{thread: "post", community: community}) - {:ok, tag2} = db_insert(:tag, %{thread: "job", community: community}) + {:ok, tag} = db_insert(:tag, %{thread: "POST", community: community}) + {:ok, tag2} = db_insert(:tag, %{thread: "JOB", community: community}) variables = %{thread: "POST", community: community.raw} From 63ed78a67c72446861f982ba0c899ba68de5269d Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 28 Oct 2018 23:27:23 +0800 Subject: [PATCH 111/129] chore(clean up): wip tag --- test/mastani_server/cms/cms_test.exs | 1 - test/mastani_server_web/mutation/cms/cms_test.exs | 3 --- test/mastani_server_web/query/cms/cms_test.exs | 3 --- 3 files changed, 7 deletions(-) diff --git a/test/mastani_server/cms/cms_test.exs b/test/mastani_server/cms/cms_test.exs index f24363627..6880f93e5 100644 --- a/test/mastani_server/cms/cms_test.exs +++ b/test/mastani_server/cms/cms_test.exs @@ -15,7 +15,6 @@ defmodule MastaniServer.Test.CMS do end describe "[cms tag]" do - @tag :wip2 test "create tag with valid data", ~m(community user)a do valid_attrs = mock_attrs(:tag, %{community_id: community.id}) diff --git a/test/mastani_server_web/mutation/cms/cms_test.exs b/test/mastani_server_web/mutation/cms/cms_test.exs index 3517d9d7d..5d81e18fb 100644 --- a/test/mastani_server_web/mutation/cms/cms_test.exs +++ b/test/mastani_server_web/mutation/cms/cms_test.exs @@ -202,7 +202,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do } } """ - @tag :wip2 test "create tag with valid attrs, has default POST thread", ~m(community)a do variables = mock_attrs(:tag, %{communityId: community.id}) @@ -220,7 +219,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do assert belong_community["id"] == to_string(community.id) end - @tag :wip2 test "auth user create duplicate tag fails", ~m(community)a do variables = mock_attrs(:tag, %{communityId: community.id}) passport_rules = %{community.title => %{"post.tag.create" => true}} @@ -267,7 +265,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do } } """ - @tag :wip test "auth user can update a tag", ~m(tag community)a do variables = %{id: tag.id, color: "GREEN", title: "new title", communityId: community.id} diff --git a/test/mastani_server_web/query/cms/cms_test.exs b/test/mastani_server_web/query/cms/cms_test.exs index 7c727178d..435370027 100644 --- a/test/mastani_server_web/query/cms/cms_test.exs +++ b/test/mastani_server_web/query/cms/cms_test.exs @@ -260,7 +260,6 @@ defmodule MastaniServer.Test.Query.CMS.Basic do } } """ - @tag :wip2 test "guest user can get partial tags by communityId and thread", ~m(guest_conn community)a do {:ok, tag} = db_insert(:tag, %{thread: "POST", community: community}) {:ok, tag2} = db_insert(:tag, %{thread: "JOB", community: community}) @@ -273,7 +272,6 @@ defmodule MastaniServer.Test.Query.CMS.Basic do assert results |> Enum.any?(&(&1["id"] != to_string(tag2.id))) end - @tag :wip2 test "user can get partial tags by topic", ~m(guest_conn community user)a do valid_attrs = mock_attrs(:tag, %{community_id: community.id}) {:ok, _tag} = CMS.create_tag(:post, valid_attrs, %User{id: user.id}) @@ -299,7 +297,6 @@ defmodule MastaniServer.Test.Query.CMS.Basic do } } """ - @tag :wip2 test "guest user can get partial tags by communityRaw", ~m(guest_conn community)a do {:ok, tag} = db_insert(:tag, %{thread: "POST", community: community}) {:ok, tag2} = db_insert(:tag, %{thread: "JOB", community: community}) From 38671853ca88d918e182380d5c24ed07ee7839a8 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Tue, 30 Oct 2018 23:08:28 +0800 Subject: [PATCH 112/129] fix(topic): default args error when topic create --- lib/mastani_server/cms/cms.ex | 1 - .../cms/delegates/article_curd.ex | 11 ++- lib/mastani_server_web/schema/cms/cms_misc.ex | 6 ++ .../schema/cms/cms_queries.ex | 1 - .../schema/cms/mutations/community.ex | 2 +- .../schema/cms/mutations/post.ex | 3 +- .../mutation/cms/post_test.exs | 20 ++++- .../query/cms/posts_topic_test.exs | 76 ++++++++++++++++--- 8 files changed, 101 insertions(+), 19 deletions(-) diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index 20ee4b61f..3eb3a4be8 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -64,7 +64,6 @@ defmodule MastaniServer.CMS do defdelegate read_content(thread, id, user), to: ArticleCURD defdelegate paged_contents(queryable, filter), to: ArticleCURD defdelegate create_content(community, thread, attrs, user), to: ArticleCURD - defdelegate create_content(community, thread, attrs, user, options), to: ArticleCURD defdelegate reaction_users(thread, react, id, filters), to: ArticleCURD # ArticleReaction diff --git a/lib/mastani_server/cms/delegates/article_curd.ex b/lib/mastani_server/cms/delegates/article_curd.ex index a77508e1a..64859f165 100644 --- a/lib/mastani_server/cms/delegates/article_curd.ex +++ b/lib/mastani_server/cms/delegates/article_curd.ex @@ -122,8 +122,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do %Community{id: community_id}, thread, attrs, - %User{id: user_id}, - options \\ %{topic: "index"} + %User{id: user_id} ) do with {:ok, author} <- ensure_author_exists(%User{id: user_id}), {:ok, action} <- match_action(thread, :community), @@ -140,7 +139,13 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do ArticleOperation.set_community(community, thread, content.id) end) |> Multi.run(:set_topic, fn %{add_content_author: content} -> - ArticleOperation.set_topic(%Topic{title: options.topic}, thread, content.id) + topic_title = + case attrs |> Map.has_key?(:topic) do + true -> attrs.topic + false -> "INDEX" + end + + ArticleOperation.set_topic(%Topic{title: topic_title}, thread, content.id) end) |> Multi.run(:set_community_flag, fn %{add_content_author: content} -> # TODO: remove this judge, as content should have a flag diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index f1d557761..efd540b3a 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -68,11 +68,17 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do value(:repo) value(:wiki) value(:cheatsheet) + # for home pages + value(:city) + value(:share) + value(:news) end enum :cms_topic do value(:index) value(:city) + value(:share) + value(:news) end enum :order_enum do diff --git a/lib/mastani_server_web/schema/cms/cms_queries.ex b/lib/mastani_server_web/schema/cms/cms_queries.ex index 6a052185f..1075ad177 100644 --- a/lib/mastani_server_web/schema/cms/cms_queries.ex +++ b/lib/mastani_server_web/schema/cms/cms_queries.ex @@ -73,7 +73,6 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do @desc "get paged posts" field :paged_posts, :paged_posts do arg(:filter, non_null(:paged_posts_filter)) - # arg(:filter, non_null(:paged_article_filter)) middleware(M.PageSizeProof) resolve(&R.CMS.paged_posts/3) diff --git a/lib/mastani_server_web/schema/cms/mutations/community.ex b/lib/mastani_server_web/schema/cms/mutations/community.ex index cb999c3e7..2eb8fcf4f 100644 --- a/lib/mastani_server_web/schema/cms/mutations/community.ex +++ b/lib/mastani_server_web/schema/cms/mutations/community.ex @@ -80,7 +80,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Community do @desc "create independent thread" field :create_thread, :thread do arg(:title, non_null(:string)) - arg(:raw, non_null(:cms_thread)) + arg(:raw, non_null(:string)) arg(:index, :integer, default_value: 0) middleware(M.Authorize, :login) diff --git a/lib/mastani_server_web/schema/cms/mutations/post.ex b/lib/mastani_server_web/schema/cms/mutations/post.ex index a8498758f..201078d34 100644 --- a/lib/mastani_server_web/schema/cms/mutations/post.ex +++ b/lib/mastani_server_web/schema/cms/mutations/post.ex @@ -15,7 +15,8 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Post do arg(:copy_right, :string) arg(:community_id, non_null(:id)) arg(:thread, :cms_thread, default_value: :post) - arg(:topic, :cms_topic, default_value: :index) + # arg(:topic, :cms_topic, default_value: :index) + arg(:topic, :string, default_value: "INDEX") arg(:tags, list_of(:ids)) middleware(M.Authorize, :login) diff --git a/test/mastani_server_web/mutation/cms/post_test.exs b/test/mastani_server_web/mutation/cms/post_test.exs index be8c30724..ba4c69a16 100644 --- a/test/mastani_server_web/mutation/cms/post_test.exs +++ b/test/mastani_server_web/mutation/cms/post_test.exs @@ -16,8 +16,24 @@ defmodule MastaniServer.Test.Mutation.Post do describe "[mutation post curd]" do @create_post_query """ - mutation($title: String!, $body: String!, $digest: String!, $length: Int!, $communityId: ID!, $tags: [Ids]){ - createPost(title: $title, body: $body, digest: $digest, length: $length, communityId: $communityId, tags: $tags) { + mutation( + $title: String! + $body: String! + $digest: String! + $length: Int! + $communityId: ID! + $tags: [Ids] + $topic: CmsTopic + ) { + createPost( + title: $title + body: $body + digest: $digest + length: $length + communityId: $communityId + tags: $tags + topic: $topic + ) { title body id diff --git a/test/mastani_server_web/query/cms/posts_topic_test.exs b/test/mastani_server_web/query/cms/posts_topic_test.exs index 497423fbd..4a04a538f 100644 --- a/test/mastani_server_web/query/cms/posts_topic_test.exs +++ b/test/mastani_server_web/query/cms/posts_topic_test.exs @@ -2,15 +2,16 @@ defmodule MastaniServer.Test.Query.PostsTopic do use MastaniServer.TestTools # import Helper.Utils, only: [get_config: 2] + alias Helper.ORM alias MastaniServer.CMS setup do {:ok, user} = db_insert(:user) {:ok, community} = db_insert(:community) - post_attrs = mock_attrs(:post, %{community_id: community.id}) - {:ok, _post} = CMS.create_content(community, :post, post_attrs, user, %{topic: "index"}) - post_attrs = mock_attrs(:post, %{community_id: community.id}) - {:ok, _post} = CMS.create_content(community, :post, post_attrs, user, %{topic: "index"}) + post_attrs = mock_attrs(:post, %{community_id: community.id, topic: "INDEX"}) + {:ok, _post} = CMS.create_content(community, :post, post_attrs, user) + post_attrs = mock_attrs(:post, %{community_id: community.id, topic: "INDEX"}) + {:ok, _post} = CMS.create_content(community, :post, post_attrs, user) guest_conn = simu_conn(:guest) @@ -18,6 +19,59 @@ defmodule MastaniServer.Test.Query.PostsTopic do end describe "[query posts topic filter]" do + @create_post_query """ + mutation( + $title: String! + $body: String! + $digest: String! + $length: Int! + $communityId: ID! + $tags: [Ids] + $topic: CmsTopic + ) { + createPost( + title: $title + body: $body + digest: $digest + length: $length + communityId: $communityId + tags: $tags + topic: $topic + ) { + title + body + id + } + } + """ + @query """ + query($filter: PagedPostsFilter!) { + pagedPosts(filter: $filter) { + entries { + id + title + } + totalCount + } + } + """ + @tag :wip + test "create post with valid args and topic ", ~m(guest_conn)a do + {:ok, user} = db_insert(:user) + user_conn = simu_conn(:user, user) + + {:ok, community} = db_insert(:community) + post_attr = mock_attrs(:post) + + variables = post_attr |> Map.merge(%{communityId: community.id, topic: "CITY"}) + created = user_conn |> mutation_result(@create_post_query, variables, "createPost") + + variables = %{filter: %{page: 1, size: 10, topic: "CITY"}} + results = guest_conn |> query_result(@query, variables, "pagedPosts") + + assert results["totalCount"] == 1 + end + @query """ query($filter: PagedPostsFilter!) { pagedPosts(filter: $filter) { @@ -29,16 +83,17 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ + @tag :wip test "topic filter on posts should work", ~m(guest_conn)a do variables = %{filter: %{page: 1, size: 10}} results = guest_conn |> query_result(@query, variables, "pagedPosts") assert results["totalCount"] == 2 - variables = %{filter: %{page: 1, size: 10, topic: "index"}} + variables = %{filter: %{page: 1, size: 10, topic: "INDEX"}} results = guest_conn |> query_result(@query, variables, "pagedPosts") assert results["totalCount"] == 2 - variables = %{filter: %{page: 1, size: 10, topic: "city"}} + variables = %{filter: %{page: 1, size: 10, topic: "OTHER"}} results = guest_conn |> query_result(@query, variables, "pagedPosts") assert results["totalCount"] == 0 end @@ -56,12 +111,13 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ + @tag :wip2 test "topic filter on non-posts has no effect", ~m(guest_conn user community)a do - job_attrs = mock_attrs(:job, %{community_id: community.id}) - {:ok, _} = CMS.create_content(community, :job, job_attrs, user, %{topic: "index"}) + job_attrs = mock_attrs(:job, %{community_id: community.id, topic: "index"}) + {:ok, _} = CMS.create_content(community, :job, job_attrs, user) - job_attrs = mock_attrs(:job, %{community_id: community.id}) - {:ok, _} = CMS.create_content(community, :job, job_attrs, user, %{topic: "city"}) + job_attrs = mock_attrs(:job, %{community_id: community.id, topic: "city"}) + {:ok, _} = CMS.create_content(community, :job, job_attrs, user) variables = %{filter: %{community: community.raw, page: 1, size: 10}} results = guest_conn |> query_result(@query, variables, "pagedJobs") From 5e2a0b45d6907bd104030da70810cb3b63a11b38 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 31 Oct 2018 11:38:04 +0800 Subject: [PATCH 113/129] refactor(general): make thread & topic downcase in db --- lib/helper/utils.ex | 2 +- .../cms/delegates/community_curd.ex | 23 +++++++++++-------- .../schema/cms/cms_queries.ex | 2 +- .../schema/cms/mutations/community.ex | 2 +- test/mastani_server/cms/cms_test.exs | 1 + .../mutation/cms/cms_test.exs | 8 +++++-- .../mastani_server_web/query/cms/cms_test.exs | 19 ++++++++------- .../query/cms/posts_topic_test.exs | 2 +- 8 files changed, 36 insertions(+), 23 deletions(-) diff --git a/lib/helper/utils.ex b/lib/helper/utils.ex index 48652e8fb..3e602a763 100644 --- a/lib/helper/utils.ex +++ b/lib/helper/utils.ex @@ -122,7 +122,7 @@ defmodule Helper.Utils do results = Enum.map(attrs, fn {k, v} -> if is_atom(v) do - {k, v |> to_string() |> String.upcase()} + {k, v |> to_string() |> String.downcase()} else {k, v} end diff --git a/lib/mastani_server/cms/delegates/community_curd.ex b/lib/mastani_server/cms/delegates/community_curd.ex index 6a0f23d49..055c80d35 100644 --- a/lib/mastani_server/cms/delegates/community_curd.ex +++ b/lib/mastani_server/cms/delegates/community_curd.ex @@ -57,10 +57,12 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do {:ok, author} <- ensure_author_exists(%Accounts.User{id: user_id}), {:ok, _community} <- ORM.find(Community, attrs.community_id), {:ok, topic} = find_or_insert_topic(attrs) do + attrs = attrs |> Map.merge(%{author_id: author.id, topic_id: topic.id}) |> map_atom_value(:string) + |> Map.merge(%{thread: attrs.thread |> to_string |> String.downcase()}) action.reactor |> ORM.create(attrs) end @@ -79,8 +81,10 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do get tags belongs to a community / thread """ def get_tags(%Community{id: community_id}, thread, topic) when not is_nil(community_id) do - thread = thread |> to_string |> String.upcase() - topic = topic |> to_string |> String.upcase() + # thread = thread |> to_string |> String.upcase() + # topic = topic |> to_string |> String.upcase() + thread = thread |> to_string |> String.downcase() + topic = topic |> to_string |> String.downcase() Tag |> join(:inner, [t], c in assoc(t, :community)) @@ -92,7 +96,8 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do end def get_tags(%Community{id: community_id}, thread) when not is_nil(community_id) do - thread = thread |> to_string |> String.upcase() + # thread = thread |> to_string |> String.upcase() + thread = thread |> to_string |> String.downcase() Tag |> join(:inner, [t], c in assoc(t, :community)) @@ -103,7 +108,7 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do end def get_tags(%Community{raw: community_raw}, thread) do - thread = thread |> to_string |> String.upcase() + thread = thread |> to_string |> String.downcase() Tag |> join(:inner, [t], c in assoc(t, :community)) @@ -163,9 +168,9 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do end end - defp find_or_insert_topic(%{topic: title} = attrs) when is_atom(title) do - title = title |> to_string() |> String.upcase() - thread = attrs.thread |> to_string() |> String.upcase() + defp find_or_insert_topic(%{topic: title} = attrs) when is_binary(title) do + title = title |> to_string() |> String.downcase() + thread = attrs.thread |> to_string() |> String.downcase() ORM.findby_or_insert(Topic, %{title: title}, %{ title: title, @@ -175,10 +180,10 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do end defp find_or_insert_topic(%{thread: thread}) do - find_or_insert_topic(%{topic: :index, thread: thread}) + find_or_insert_topic(%{topic: "index", thread: thread}) end defp find_or_insert_topic(_attrs) do - find_or_insert_topic(%{topic: :index, thread: :post}) + find_or_insert_topic(%{topic: "index", thread: :post}) end end diff --git a/lib/mastani_server_web/schema/cms/cms_queries.ex b/lib/mastani_server_web/schema/cms/cms_queries.ex index 1075ad177..6e432fe73 100644 --- a/lib/mastani_server_web/schema/cms/cms_queries.ex +++ b/lib/mastani_server_web/schema/cms/cms_queries.ex @@ -167,7 +167,7 @@ defmodule MastaniServerWeb.Schema.CMS.Queries do arg(:community_id, :id) arg(:community, :string) arg(:thread, :cms_thread, default_value: :post) - arg(:topic, :cms_topic) + arg(:topic, :string) resolve(&R.CMS.get_tags/3) end diff --git a/lib/mastani_server_web/schema/cms/mutations/community.ex b/lib/mastani_server_web/schema/cms/mutations/community.ex index 2eb8fcf4f..1d1c485ed 100644 --- a/lib/mastani_server_web/schema/cms/mutations/community.ex +++ b/lib/mastani_server_web/schema/cms/mutations/community.ex @@ -134,7 +134,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Community do arg(:color, non_null(:rainbow_color_enum)) arg(:community_id, non_null(:id)) arg(:thread, :cms_thread, default_value: :post) - arg(:topic, :cms_topic, default_value: :index) + arg(:topic, :string, default_value: "index") middleware(M.Authorize, :login) middleware(M.PassportLoader, source: :community) diff --git a/test/mastani_server/cms/cms_test.exs b/test/mastani_server/cms/cms_test.exs index 6880f93e5..c6ac730c6 100644 --- a/test/mastani_server/cms/cms_test.exs +++ b/test/mastani_server/cms/cms_test.exs @@ -15,6 +15,7 @@ defmodule MastaniServer.Test.CMS do end describe "[cms tag]" do + @tag :wip test "create tag with valid data", ~m(community user)a do valid_attrs = mock_attrs(:tag, %{community_id: community.id}) diff --git a/test/mastani_server_web/mutation/cms/cms_test.exs b/test/mastani_server_web/mutation/cms/cms_test.exs index 5d81e18fb..b04d399de 100644 --- a/test/mastani_server_web/mutation/cms/cms_test.exs +++ b/test/mastani_server_web/mutation/cms/cms_test.exs @@ -202,6 +202,7 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do } } """ + @tag :wip test "create tag with valid attrs, has default POST thread", ~m(community)a do variables = mock_attrs(:tag, %{communityId: community.id}) @@ -209,16 +210,18 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do rule_conn = simu_conn(:user, cms: passport_rules) # IO.inspect variables, label: "hello variables" + created = rule_conn |> mutation_result(@create_tag_query, variables, "createTag") belong_community = created["community"] {:ok, found} = Tag |> ORM.find(created["id"]) assert created["id"] == to_string(found.id) - assert found.thread == "POST" + assert found.thread == "post" assert belong_community["id"] == to_string(community.id) end + @tag :wip test "auth user create duplicate tag fails", ~m(community)a do variables = mock_attrs(:tag, %{communityId: community.id}) passport_rules = %{community.title => %{"post.tag.create" => true}} @@ -265,6 +268,7 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do } } """ + @tag :wip test "auth user can update a tag", ~m(tag community)a do variables = %{id: tag.id, color: "GREEN", title: "new title", communityId: community.id} @@ -273,7 +277,7 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do updated = rule_conn |> mutation_result(@update_tag_query, variables, "updateTag") - assert updated["color"] == "GREEN" + assert updated["color"] == "green" assert updated["title"] == "new title" end diff --git a/test/mastani_server_web/query/cms/cms_test.exs b/test/mastani_server_web/query/cms/cms_test.exs index 435370027..e72b0188a 100644 --- a/test/mastani_server_web/query/cms/cms_test.exs +++ b/test/mastani_server_web/query/cms/cms_test.exs @@ -246,7 +246,7 @@ defmodule MastaniServer.Test.Query.CMS.Basic do end @query """ - query($communityId: ID, $thread: CmsThread!, $topic: CmsTopic ) { + query($communityId: ID, $thread: CmsThread!, $topic: String ) { partialTags(communityId: $communityId, thread: $thread, topic: $topic) { id title @@ -260,9 +260,10 @@ defmodule MastaniServer.Test.Query.CMS.Basic do } } """ + @tag :wip test "guest user can get partial tags by communityId and thread", ~m(guest_conn community)a do - {:ok, tag} = db_insert(:tag, %{thread: "POST", community: community}) - {:ok, tag2} = db_insert(:tag, %{thread: "JOB", community: community}) + {:ok, tag} = db_insert(:tag, %{thread: "post", community: community}) + {:ok, tag2} = db_insert(:tag, %{thread: "job", community: community}) variables = %{thread: "POST", communityId: community.id} @@ -272,13 +273,14 @@ defmodule MastaniServer.Test.Query.CMS.Basic do assert results |> Enum.any?(&(&1["id"] != to_string(tag2.id))) end - test "user can get partial tags by topic", ~m(guest_conn community user)a do + @tag :wip + test "user can get partial tags by default index topic", ~m(guest_conn community user)a do valid_attrs = mock_attrs(:tag, %{community_id: community.id}) {:ok, _tag} = CMS.create_tag(:post, valid_attrs, %User{id: user.id}) - variables = %{thread: "POST", communityId: community.id, topic: "INDEX"} - + variables = %{thread: "POST", communityId: community.id, topic: "index"} results = guest_conn |> query_result(@query, variables, "partialTags") + assert results |> length == 1 end @@ -297,9 +299,10 @@ defmodule MastaniServer.Test.Query.CMS.Basic do } } """ + @tag :wip test "guest user can get partial tags by communityRaw", ~m(guest_conn community)a do - {:ok, tag} = db_insert(:tag, %{thread: "POST", community: community}) - {:ok, tag2} = db_insert(:tag, %{thread: "JOB", community: community}) + {:ok, tag} = db_insert(:tag, %{thread: "post", community: community}) + {:ok, tag2} = db_insert(:tag, %{thread: "job", community: community}) variables = %{thread: "POST", community: community.raw} diff --git a/test/mastani_server_web/query/cms/posts_topic_test.exs b/test/mastani_server_web/query/cms/posts_topic_test.exs index 4a04a538f..74b9b90a3 100644 --- a/test/mastani_server_web/query/cms/posts_topic_test.exs +++ b/test/mastani_server_web/query/cms/posts_topic_test.exs @@ -111,7 +111,7 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ - @tag :wip2 + @tag :wip test "topic filter on non-posts has no effect", ~m(guest_conn user community)a do job_attrs = mock_attrs(:job, %{community_id: community.id, topic: "index"}) {:ok, _} = CMS.create_content(community, :job, job_attrs, user) From e68b08b34fe3264bd370b76ff7aaf4852541cafe Mon Sep 17 00:00:00 2001 From: mydearxym Date: Wed, 31 Oct 2018 23:27:36 +0800 Subject: [PATCH 114/129] fix(passport): community check use raw, not title --- lib/mastani_server/cms/delegates/community_curd.ex | 1 - lib/mastani_server_web/middleware/passport.ex | 10 +++++++--- lib/mastani_server_web/schema/cms/cms_types.ex | 7 +++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/mastani_server/cms/delegates/community_curd.ex b/lib/mastani_server/cms/delegates/community_curd.ex index 055c80d35..639c771ac 100644 --- a/lib/mastani_server/cms/delegates/community_curd.ex +++ b/lib/mastani_server/cms/delegates/community_curd.ex @@ -57,7 +57,6 @@ defmodule MastaniServer.CMS.Delegate.CommunityCURD do {:ok, author} <- ensure_author_exists(%Accounts.User{id: user_id}), {:ok, _community} <- ORM.find(Community, attrs.community_id), {:ok, topic} = find_or_insert_topic(attrs) do - attrs = attrs |> Map.merge(%{author_id: author.id, topic_id: topic.id}) diff --git a/lib/mastani_server_web/middleware/passport.ex b/lib/mastani_server_web/middleware/passport.ex index 36621a5e1..2c0896a2c 100644 --- a/lib/mastani_server_web/middleware/passport.ex +++ b/lib/mastani_server_web/middleware/passport.ex @@ -16,7 +16,9 @@ defmodule MastaniServerWeb.Middleware.Passport do import Helper.Utils import Helper.ErrorCode - def call(%{errors: errors} = resolution, _) when length(errors) > 0, do: resolution + def call(%{errors: errors} = resolution, _) when length(errors) > 0 do + resolution + end def call(%{arguments: %{passport_is_owner: true}} = resolution, claim: "owner"), do: resolution @@ -131,13 +133,15 @@ defmodule MastaniServerWeb.Middleware.Passport do defp cp_check(resolution, claim) do cur_passport = resolution.context.cur_user.cur_passport - community_title = resolution.arguments.passport_communities |> List.first() |> Map.get(:title) + # community_title = resolution.arguments.passport_communities |> List.first() |> Map.get(:title) + community_raw = resolution.arguments.passport_communities |> List.first() |> Map.get(:raw) thread = resolution.arguments.thread |> to_string path = claim - |> String.replace("c?", community_title) + # |> String.replace("c?", community_title) + |> String.replace("c?", community_raw) |> String.replace("t?", thread) |> String.split("->") diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 156d25831..678b39a16 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -328,6 +328,12 @@ defmodule MastaniServerWeb.Schema.CMS.Types do timestamp_fields() end + object :topic do + field(:id, :id) + field(:title, :string) + field(:raw, :string) + end + object :tag do field(:id, :id) field(:title, :string) @@ -335,6 +341,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:thread, :string) field(:author, :user, resolve: dataloader(CMS, :author)) field(:community, :community, resolve: dataloader(CMS, :community)) + field(:topic, :topic, resolve: dataloader(CMS, :topic)) timestamp_fields() end From 9697c412f61cf9537a3d137f4b72d3224e80d546 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 1 Nov 2018 11:56:23 +0800 Subject: [PATCH 115/129] refactor(topic): change default topic to posts --- .../cms/delegates/article_curd.ex | 2 +- lib/mastani_server_web/schema/cms/cms_misc.ex | 1 - .../schema/cms/mutations/community.ex | 2 +- .../schema/cms/mutations/post.ex | 3 +- test/mastani_server/cms/cms_test.exs | 1 - .../mutation/cms/cms_test.exs | 38 +++++++++++++++---- .../query/cms/posts_topic_test.exs | 16 ++++---- 7 files changed, 42 insertions(+), 21 deletions(-) diff --git a/lib/mastani_server/cms/delegates/article_curd.ex b/lib/mastani_server/cms/delegates/article_curd.ex index 64859f165..14daa0e75 100644 --- a/lib/mastani_server/cms/delegates/article_curd.ex +++ b/lib/mastani_server/cms/delegates/article_curd.ex @@ -142,7 +142,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do topic_title = case attrs |> Map.has_key?(:topic) do true -> attrs.topic - false -> "INDEX" + false -> "posts" end ArticleOperation.set_topic(%Topic{title: topic_title}, thread, content.id) diff --git a/lib/mastani_server_web/schema/cms/cms_misc.ex b/lib/mastani_server_web/schema/cms/cms_misc.ex index efd540b3a..937af704d 100644 --- a/lib/mastani_server_web/schema/cms/cms_misc.ex +++ b/lib/mastani_server_web/schema/cms/cms_misc.ex @@ -201,7 +201,6 @@ defmodule MastaniServerWeb.Schema.CMS.Misc do field(:sort, :sort_enum) field(:tag, :string, default_value: :all) field(:community, :string) - # field(:topic, :string, default_value: "index") field(:topic, :string) end diff --git a/lib/mastani_server_web/schema/cms/mutations/community.ex b/lib/mastani_server_web/schema/cms/mutations/community.ex index 1d1c485ed..f1b4ed5ec 100644 --- a/lib/mastani_server_web/schema/cms/mutations/community.ex +++ b/lib/mastani_server_web/schema/cms/mutations/community.ex @@ -134,7 +134,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Community do arg(:color, non_null(:rainbow_color_enum)) arg(:community_id, non_null(:id)) arg(:thread, :cms_thread, default_value: :post) - arg(:topic, :string, default_value: "index") + arg(:topic, :string, default_value: "posts") middleware(M.Authorize, :login) middleware(M.PassportLoader, source: :community) diff --git a/lib/mastani_server_web/schema/cms/mutations/post.ex b/lib/mastani_server_web/schema/cms/mutations/post.ex index 201078d34..4a87301eb 100644 --- a/lib/mastani_server_web/schema/cms/mutations/post.ex +++ b/lib/mastani_server_web/schema/cms/mutations/post.ex @@ -15,8 +15,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Post do arg(:copy_right, :string) arg(:community_id, non_null(:id)) arg(:thread, :cms_thread, default_value: :post) - # arg(:topic, :cms_topic, default_value: :index) - arg(:topic, :string, default_value: "INDEX") + arg(:topic, :string, default_value: "posts") arg(:tags, list_of(:ids)) middleware(M.Authorize, :login) diff --git a/test/mastani_server/cms/cms_test.exs b/test/mastani_server/cms/cms_test.exs index c6ac730c6..6880f93e5 100644 --- a/test/mastani_server/cms/cms_test.exs +++ b/test/mastani_server/cms/cms_test.exs @@ -15,7 +15,6 @@ defmodule MastaniServer.Test.CMS do end describe "[cms tag]" do - @tag :wip test "create tag with valid data", ~m(community user)a do valid_attrs = mock_attrs(:tag, %{community_id: community.id}) diff --git a/test/mastani_server_web/mutation/cms/cms_test.exs b/test/mastani_server_web/mutation/cms/cms_test.exs index b04d399de..62859f016 100644 --- a/test/mastani_server_web/mutation/cms/cms_test.exs +++ b/test/mastani_server_web/mutation/cms/cms_test.exs @@ -187,13 +187,15 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do describe "[mutation cms tag]" do @create_tag_query """ - mutation($thread: CmsThread!, $title: String!, $color: RainbowColorEnum!, $communityId: ID!) { - createTag(thread: $thread, title: $title, color: $color, communityId: $communityId) { + mutation($thread: CmsThread!, $title: String!, $color: RainbowColorEnum!, $communityId: ID!, $topic: String) { + createTag(thread: $thread, title: $title, color: $color, communityId: $communityId, topic: $topic) { id title color thread - + topic { + title + } community { id logo @@ -203,14 +205,13 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do } """ @tag :wip - test "create tag with valid attrs, has default POST thread", ~m(community)a do + test "create tag with valid attrs, has default POST thread and default posts topic", + ~m(community)a do variables = mock_attrs(:tag, %{communityId: community.id}) passport_rules = %{community.title => %{"post.tag.create" => true}} rule_conn = simu_conn(:user, cms: passport_rules) - # IO.inspect variables, label: "hello variables" - created = rule_conn |> mutation_result(@create_tag_query, variables, "createTag") belong_community = created["community"] @@ -218,12 +219,35 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do assert created["id"] == to_string(found.id) assert found.thread == "post" + assert created["topic"]["title"] == "posts" assert belong_community["id"] == to_string(community.id) end - @tag :wip + @tag :wip2 + test "can create some tag on different topic", ~m(community)a do + variables = mock_attrs(:tag, %{communityId: community.id, topic: "city"}) + + passport_rules = %{community.title => %{"post.tag.create" => true}} + rule_conn = simu_conn(:user, cms: passport_rules) + + created = rule_conn |> mutation_result(@create_tag_query, variables, "createTag") + assert created["title"] == variables.title + assert created["topic"]["title"] == "city" + + assert rule_conn + |> mutation_get_error?(@create_tag_query, variables) + + variables = variables |> Map.merge(%{topic: "news"}) + created = rule_conn |> mutation_result(@create_tag_query, variables, "createTag") + assert created["title"] == variables.title + assert created["topic"]["title"] == "news" + end + + @tag :wip2 test "auth user create duplicate tag fails", ~m(community)a do variables = mock_attrs(:tag, %{communityId: community.id}) + # IO.inspect variables, label: "hello variables" + passport_rules = %{community.title => %{"post.tag.create" => true}} rule_conn = simu_conn(:user, cms: passport_rules) diff --git a/test/mastani_server_web/query/cms/posts_topic_test.exs b/test/mastani_server_web/query/cms/posts_topic_test.exs index 74b9b90a3..8f0a8c61c 100644 --- a/test/mastani_server_web/query/cms/posts_topic_test.exs +++ b/test/mastani_server_web/query/cms/posts_topic_test.exs @@ -8,9 +8,9 @@ defmodule MastaniServer.Test.Query.PostsTopic do setup do {:ok, user} = db_insert(:user) {:ok, community} = db_insert(:community) - post_attrs = mock_attrs(:post, %{community_id: community.id, topic: "INDEX"}) + post_attrs = mock_attrs(:post, %{community_id: community.id, topic: "posts"}) {:ok, _post} = CMS.create_content(community, :post, post_attrs, user) - post_attrs = mock_attrs(:post, %{community_id: community.id, topic: "INDEX"}) + post_attrs = mock_attrs(:post, %{community_id: community.id, topic: "posts"}) {:ok, _post} = CMS.create_content(community, :post, post_attrs, user) guest_conn = simu_conn(:guest) @@ -63,10 +63,10 @@ defmodule MastaniServer.Test.Query.PostsTopic do {:ok, community} = db_insert(:community) post_attr = mock_attrs(:post) - variables = post_attr |> Map.merge(%{communityId: community.id, topic: "CITY"}) + variables = post_attr |> Map.merge(%{communityId: community.id, topic: "city"}) created = user_conn |> mutation_result(@create_post_query, variables, "createPost") - variables = %{filter: %{page: 1, size: 10, topic: "CITY"}} + variables = %{filter: %{page: 1, size: 10, topic: "city"}} results = guest_conn |> query_result(@query, variables, "pagedPosts") assert results["totalCount"] == 1 @@ -89,11 +89,11 @@ defmodule MastaniServer.Test.Query.PostsTopic do results = guest_conn |> query_result(@query, variables, "pagedPosts") assert results["totalCount"] == 2 - variables = %{filter: %{page: 1, size: 10, topic: "INDEX"}} + variables = %{filter: %{page: 1, size: 10, topic: "posts"}} results = guest_conn |> query_result(@query, variables, "pagedPosts") assert results["totalCount"] == 2 - variables = %{filter: %{page: 1, size: 10, topic: "OTHER"}} + variables = %{filter: %{page: 1, size: 10, topic: "other"}} results = guest_conn |> query_result(@query, variables, "pagedPosts") assert results["totalCount"] == 0 end @@ -113,7 +113,7 @@ defmodule MastaniServer.Test.Query.PostsTopic do """ @tag :wip test "topic filter on non-posts has no effect", ~m(guest_conn user community)a do - job_attrs = mock_attrs(:job, %{community_id: community.id, topic: "index"}) + job_attrs = mock_attrs(:job, %{community_id: community.id, topic: "posts"}) {:ok, _} = CMS.create_content(community, :job, job_attrs, user) job_attrs = mock_attrs(:job, %{community_id: community.id, topic: "city"}) @@ -123,7 +123,7 @@ defmodule MastaniServer.Test.Query.PostsTopic do results = guest_conn |> query_result(@query, variables, "pagedJobs") assert results["totalCount"] == 2 - variables = %{filter: %{page: 1, size: 10, topic: "index"}} + variables = %{filter: %{page: 1, size: 10, topic: "posts"}} assert guest_conn |> query_get_error?(@query, variables) end end From d6510de27deaade3fd7f780f38f8f626a5e20ae2 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 1 Nov 2018 11:59:36 +0800 Subject: [PATCH 116/129] chore(clean up): wip tag --- test/mastani_server_web/mutation/cms/cms_test.exs | 4 ---- test/mastani_server_web/query/cms/cms_test.exs | 3 --- test/mastani_server_web/query/cms/posts_topic_test.exs | 3 --- 3 files changed, 10 deletions(-) diff --git a/test/mastani_server_web/mutation/cms/cms_test.exs b/test/mastani_server_web/mutation/cms/cms_test.exs index 62859f016..6f99b2aae 100644 --- a/test/mastani_server_web/mutation/cms/cms_test.exs +++ b/test/mastani_server_web/mutation/cms/cms_test.exs @@ -204,7 +204,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do } } """ - @tag :wip test "create tag with valid attrs, has default POST thread and default posts topic", ~m(community)a do variables = mock_attrs(:tag, %{communityId: community.id}) @@ -223,7 +222,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do assert belong_community["id"] == to_string(community.id) end - @tag :wip2 test "can create some tag on different topic", ~m(community)a do variables = mock_attrs(:tag, %{communityId: community.id, topic: "city"}) @@ -243,7 +241,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do assert created["topic"]["title"] == "news" end - @tag :wip2 test "auth user create duplicate tag fails", ~m(community)a do variables = mock_attrs(:tag, %{communityId: community.id}) # IO.inspect variables, label: "hello variables" @@ -292,7 +289,6 @@ defmodule MastaniServer.Test.Mutation.CMS.Basic do } } """ - @tag :wip test "auth user can update a tag", ~m(tag community)a do variables = %{id: tag.id, color: "GREEN", title: "new title", communityId: community.id} diff --git a/test/mastani_server_web/query/cms/cms_test.exs b/test/mastani_server_web/query/cms/cms_test.exs index e72b0188a..fb50439c6 100644 --- a/test/mastani_server_web/query/cms/cms_test.exs +++ b/test/mastani_server_web/query/cms/cms_test.exs @@ -260,7 +260,6 @@ defmodule MastaniServer.Test.Query.CMS.Basic do } } """ - @tag :wip test "guest user can get partial tags by communityId and thread", ~m(guest_conn community)a do {:ok, tag} = db_insert(:tag, %{thread: "post", community: community}) {:ok, tag2} = db_insert(:tag, %{thread: "job", community: community}) @@ -273,7 +272,6 @@ defmodule MastaniServer.Test.Query.CMS.Basic do assert results |> Enum.any?(&(&1["id"] != to_string(tag2.id))) end - @tag :wip test "user can get partial tags by default index topic", ~m(guest_conn community user)a do valid_attrs = mock_attrs(:tag, %{community_id: community.id}) {:ok, _tag} = CMS.create_tag(:post, valid_attrs, %User{id: user.id}) @@ -299,7 +297,6 @@ defmodule MastaniServer.Test.Query.CMS.Basic do } } """ - @tag :wip test "guest user can get partial tags by communityRaw", ~m(guest_conn community)a do {:ok, tag} = db_insert(:tag, %{thread: "post", community: community}) {:ok, tag2} = db_insert(:tag, %{thread: "job", community: community}) diff --git a/test/mastani_server_web/query/cms/posts_topic_test.exs b/test/mastani_server_web/query/cms/posts_topic_test.exs index 8f0a8c61c..1a6e1536d 100644 --- a/test/mastani_server_web/query/cms/posts_topic_test.exs +++ b/test/mastani_server_web/query/cms/posts_topic_test.exs @@ -55,7 +55,6 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ - @tag :wip test "create post with valid args and topic ", ~m(guest_conn)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -83,7 +82,6 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ - @tag :wip test "topic filter on posts should work", ~m(guest_conn)a do variables = %{filter: %{page: 1, size: 10}} results = guest_conn |> query_result(@query, variables, "pagedPosts") @@ -111,7 +109,6 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ - @tag :wip test "topic filter on non-posts has no effect", ~m(guest_conn user community)a do job_attrs = mock_attrs(:job, %{community_id: community.id, topic: "posts"}) {:ok, _} = CMS.create_content(community, :job, job_attrs, user) From cba7826c26fe4d2e66222791d6b23a845e1db827 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 1 Nov 2018 21:35:25 +0800 Subject: [PATCH 117/129] refactor(accounts): make inline-subcommunities return paged-version --- .../accounts/delegates/profile.ex | 2 ++ .../resolvers/accounts_resolver.ex | 7 ++++-- .../schema/account/account_types.ex | 13 ++++++++--- .../query/accounts/account_test.exs | 23 +++++++++++-------- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/lib/mastani_server/accounts/delegates/profile.ex b/lib/mastani_server/accounts/delegates/profile.ex index 6783ef179..b4a5a725c 100644 --- a/lib/mastani_server/accounts/delegates/profile.ex +++ b/lib/mastani_server/accounts/delegates/profile.ex @@ -75,6 +75,8 @@ defmodule MastaniServer.Accounts.Delegate.Profile do get users subscribed communities """ def subscribed_communities(%User{id: id}, %{page: page, size: size} = filter) do + filter = filter |> Map.delete(:first) + CMS.CommunitySubscriber |> where([c], c.user_id == ^id) |> join(:inner, [c], cc in assoc(c, :community)) diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 6e30e0adf..a033b8a2b 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -35,7 +35,7 @@ defmodule MastaniServerWeb.Resolvers.Accounts do end def github_signin(_root, %{github_user: github_user}, %{remote_ip: remote_ip}) do - IO.inspect(remote_ip, label: "remote_ip") + # IO.inspect(remote_ip, label: "remote_ip") Accounts.github_signin(github_user, remote_ip) end @@ -188,7 +188,10 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.subscribed_communities(%User{id: cur_user.id}, filter) end - # + def subscribed_communities(%{id: id}, %{filter: filter}, _info) do + Accounts.subscribed_communities(%User{id: id}, filter) + end + def subscribed_communities(_root, %{user_id: "", filter: filter}, _info) do Accounts.default_subscribed_communities(filter) end diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index bb4528c8a..189def474 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -65,11 +65,18 @@ defmodule MastaniServerWeb.Schema.Account.Types do resolve(&R.Accounts.get_passport/3) end - field :subscribed_communities, list_of(:community) do - arg(:filter, :members_filter) + # field :subscribed_communities, list_of(:community) do + # arg(:filter, :members_filter) + + # middleware(M.PageSizeProof) + # resolve(dataloader(Accounts, :subscribed_communities)) + # end + @desc "paged communities subscribed by this user" + field :subscribed_communities, :paged_communities do + arg(:filter, :paged_filter) middleware(M.PageSizeProof) - resolve(dataloader(Accounts, :subscribed_communities)) + resolve(&R.Accounts.subscribed_communities/3) end field :subscribed_communities_count, :integer do diff --git a/test/mastani_server_web/query/accounts/account_test.exs b/test/mastani_server_web/query/accounts/account_test.exs index a8a7079a8..a0726675e 100644 --- a/test/mastani_server_web/query/accounts/account_test.exs +++ b/test/mastani_server_web/query/accounts/account_test.exs @@ -181,15 +181,19 @@ defmodule MastaniServer.Test.Query.Account.Basic do nickname subscribedCommunitiesCount subscribedCommunities { - id - title + entries { + id + title + } + pageSize + totalCount } } } """ - test "gest user can get subscrubed community list and count", ~m(guest_conn user)a do + test "guest user can get subscrubed community list and count", ~m(guest_conn user)a do variables = %{id: user.id} - {:ok, communities} = db_insert_multi(:community, inner_page_size()) + {:ok, communities} = db_insert_multi(:community, page_size()) Enum.each( communities, @@ -197,7 +201,7 @@ defmodule MastaniServer.Test.Query.Account.Basic do ) results = guest_conn |> query_result(@query, variables, "user") - subscribed_communities = results["subscribedCommunities"] + subscribed_communities = results["subscribedCommunities"]["entries"] subscribed_communities_count = results["subscribedCommunitiesCount"] [community_1, community_2, community_3, community_x] = communities |> firstn_and_last(3) @@ -205,12 +209,12 @@ defmodule MastaniServer.Test.Query.Account.Basic do assert subscribed_communities |> Enum.any?(&(&1["id"] == to_string(community_2.id))) assert subscribed_communities |> Enum.any?(&(&1["id"] == to_string(community_3.id))) assert subscribed_communities |> Enum.any?(&(&1["id"] == to_string(community_x.id))) - assert subscribed_communities_count == inner_page_size() + assert subscribed_communities_count == page_size() end test "gest user can get subscrubed communities count of 20 at most", ~m(guest_conn user)a do variables = %{id: user.id} - {:ok, communities} = db_insert_multi(:community, inner_page_size() + 1) + {:ok, communities} = db_insert_multi(:community, page_size() + 1) Enum.each( communities, @@ -220,7 +224,8 @@ defmodule MastaniServer.Test.Query.Account.Basic do results = guest_conn |> query_result(@query, variables, "user") subscribed_communities = results["subscribedCommunities"] - assert length(subscribed_communities) == inner_page_size() + assert subscribed_communities["totalCount"] == page_size() + 1 + assert subscribed_communities["pageSize"] == page_size() end @query """ @@ -236,7 +241,7 @@ defmodule MastaniServer.Test.Query.Account.Basic do } } """ - test "gest user can get paged default subscrubed communities", ~m(guest_conn)a do + test "guest user can get paged default subscrubed communities", ~m(guest_conn)a do {:ok, _} = db_insert_multi(:community, 25) variables = %{filter: %{page: 1, size: 10}} From 73159f581df700350d82c495ec13786e2c952404 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 1 Nov 2018 22:54:57 +0800 Subject: [PATCH 118/129] refactor(account): remove account endpoint, use general user --- .../resolvers/accounts_resolver.ex | 12 ++- .../schema/account/account_queries.ex | 2 +- .../mutation/delivery/delivery_test.exs | 30 +++---- .../query/accounts/account_test.exs | 79 ++++++++++--------- .../query/accounts/achievement_test.exs | 4 +- .../query/accounts/favorited_jobs_test.exs | 4 +- .../query/accounts/favorited_posts_test.exs | 4 +- .../query/accounts/favorited_repos_test.exs | 4 +- .../query/accounts/favorited_videos_test.exs | 4 +- .../query/accounts/messages_test.exs | 20 ++--- .../query/accounts/stared_contents_test.exs | 12 +-- 11 files changed, 93 insertions(+), 82 deletions(-) diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index a033b8a2b..33d5e9b26 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -3,6 +3,7 @@ defmodule MastaniServerWeb.Resolvers.Accounts do accounts resolvers """ import ShortMaps + import Helper.ErrorCode alias Helper.{Certification, ORM} alias MastaniServer.{Accounts, CMS} @@ -10,6 +11,14 @@ defmodule MastaniServerWeb.Resolvers.Accounts do alias Accounts.{MentionMail, NotificationMail, SysNotificationMail, User} def user(_root, %{id: id}, _info), do: User |> ORM.read(id, inc: :views) + + def user(_root, _args, %{context: %{cur_user: cur_user}}), + do: User |> ORM.read(cur_user.id, inc: :views) + + def user(_root, _args, _info) do + {:error, [message: "need login", code: ecode(:account_login)]} + end + def users(_root, ~m(filter)a, _info), do: User |> ORM.find_all(filter) def session_state(_root, _args, %{context: %{cur_user: cur_user}}), @@ -17,9 +26,6 @@ defmodule MastaniServerWeb.Resolvers.Accounts do def session_state(_root, _args, _info), do: {:ok, %{is_valid: false}} - def account(_root, _args, %{context: %{cur_user: cur_user}}), - do: User |> ORM.read(cur_user.id, inc: :views) - def update_profile(_root, args, %{context: %{cur_user: cur_user}}) do profile = if Map.has_key?(args, :education_backgrounds), diff --git a/lib/mastani_server_web/schema/account/account_queries.ex b/lib/mastani_server_web/schema/account/account_queries.ex index 0cdae22ea..c11a0f41b 100644 --- a/lib/mastani_server_web/schema/account/account_queries.ex +++ b/lib/mastani_server_web/schema/account/account_queries.ex @@ -15,7 +15,7 @@ defmodule MastaniServerWeb.Schema.Account.Queries do @desc "get user by id" field :user, :user do - arg(:id, non_null(:id)) + arg(:id, :id) resolve(&R.Accounts.user/3) end diff --git a/test/mastani_server_web/mutation/delivery/delivery_test.exs b/test/mastani_server_web/mutation/delivery/delivery_test.exs index 6662d5609..c99e8d708 100644 --- a/test/mastani_server_web/mutation/delivery/delivery_test.exs +++ b/test/mastani_server_web/mutation/delivery/delivery_test.exs @@ -14,7 +14,7 @@ defmodule MastaniServer.Test.Mutation.Delivery do @account_query """ query($filter: MessagesFilter!) { - account { + user { id mentions(filter: $filter) { entries { @@ -73,7 +73,7 @@ defmodule MastaniServer.Test.Mutation.Delivery do rule_conn |> mutation_result(@query, variables, "publishSystemNotification") variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") sys_notifications = result["sysNotifications"] assert sys_notifications["totalCount"] == 1 @@ -107,7 +107,7 @@ defmodule MastaniServer.Test.Mutation.Delivery do mock_sys_notification(3) variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") notifications = result["sysNotifications"] assert notifications["totalCount"] == 3 @@ -118,14 +118,14 @@ defmodule MastaniServer.Test.Mutation.Delivery do user_conn |> mutation_result(@query, variables, "markSysNotificationRead") variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") notifications = result["sysNotifications"] assert notifications["totalCount"] == 2 variables = %{filter: %{page: 1, size: 20, read: true}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") notifications = result["sysNotifications"] assert notifications["totalCount"] == 1 end @@ -190,7 +190,7 @@ defmodule MastaniServer.Test.Mutation.Delivery do mock_mentions_for(user, 3) variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") mentions = result["mentions"] assert mentions["totalCount"] == 3 @@ -201,13 +201,13 @@ defmodule MastaniServer.Test.Mutation.Delivery do variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") mentions = result["mentions"] assert mentions["totalCount"] == 2 variables = %{filter: %{page: 1, size: 20, read: true}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") mentions = result["mentions"] assert mentions["totalCount"] == 1 end @@ -226,7 +226,7 @@ defmodule MastaniServer.Test.Mutation.Delivery do mock_mentions_for(user, 3) variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") mentions = result["mentions"] # IO.inspect mentions, label: "mentions" assert mentions["totalCount"] == 3 @@ -234,7 +234,7 @@ defmodule MastaniServer.Test.Mutation.Delivery do user_conn |> mutation_result(@query, %{}, "markMentionReadAll") variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") mentions = result["mentions"] assert mentions["totalCount"] == 0 @@ -254,7 +254,7 @@ defmodule MastaniServer.Test.Mutation.Delivery do mock_notifications_for(user, 3) variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") notifications = result["notifications"] assert notifications["totalCount"] == 3 @@ -265,14 +265,14 @@ defmodule MastaniServer.Test.Mutation.Delivery do user_conn |> mutation_result(@query, variables, "markNotificationRead") variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") notifications = result["notifications"] assert notifications["totalCount"] == 2 variables = %{filter: %{page: 1, size: 20, read: true}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") notifications = result["notifications"] assert notifications["totalCount"] == 1 end @@ -291,14 +291,14 @@ defmodule MastaniServer.Test.Mutation.Delivery do mock_notifications_for(user, 3) variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") notifications = result["notifications"] assert notifications["totalCount"] == 3 user_conn |> mutation_result(@query, %{}, "markNotificationReadAll") variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@account_query, variables, "account") + result = user_conn |> query_result(@account_query, variables, "user") notifications = result["notifications"] assert notifications["totalCount"] == 0 diff --git a/test/mastani_server_web/query/accounts/account_test.exs b/test/mastani_server_web/query/accounts/account_test.exs index a0726675e..3e562c65f 100644 --- a/test/mastani_server_web/query/accounts/account_test.exs +++ b/test/mastani_server_web/query/accounts/account_test.exs @@ -11,47 +11,13 @@ defmodule MastaniServer.Test.Query.Account.Basic do setup do {:ok, user} = db_insert(:user) guest_conn = simu_conn(:guest) - # user_conn = simu_conn(:user, user) - {:ok, ~m(guest_conn user)a} - end - - describe "[account session state]" do - @query """ - query { - sessionState { - isValid - user { - id - } - } - } - """ - test "guest user should get false sessionState", ~m(guest_conn)a do - results = guest_conn |> query_result(@query, %{}, "sessionState") - assert results["isValid"] == false - assert results["user"] == nil - end - - test "login user should get true sessionState", ~m(user)a do - user_conn = simu_conn(:user, user) - results = user_conn |> query_result(@query, %{}, "sessionState") - - assert results["isValid"] == true - assert results["user"] |> Map.get("id") == to_string(user.id) - end - - test "user with invalid token get false sessionState" do - user_conn = simu_conn(:invalid_token) - results = user_conn |> query_result(@query, %{}, "sessionState") - - assert results["isValid"] == false - assert results["user"] == nil - end + user_conn = simu_conn(:user, user) + {:ok, ~m(guest_conn user_conn user)a} end describe "[account basic]" do @query """ - query($id: ID!) { + query($id: ID) { user(id: $id) { id nickname @@ -81,6 +47,11 @@ defmodule MastaniServer.Test.Query.Account.Basic do assert results["cmsPassport"] == nil end + test "login user can get it's own profile", ~m(user_conn user)a do + results = user_conn |> query_result(@query, %{}, "user") + assert results["id"] == to_string(user.id) + end + test "user's views +1 after visit", ~m(guest_conn user)a do {:ok, target_user} = ORM.find(Accounts.User, user.id) assert target_user.views == 0 @@ -275,4 +246,38 @@ defmodule MastaniServer.Test.Query.Account.Basic do assert @default_subscribed_communities == results["pageSize"] end end + + describe "[account session state]" do + @query """ + query { + sessionState { + isValid + user { + id + } + } + } + """ + test "guest user should get false sessionState", ~m(guest_conn)a do + results = guest_conn |> query_result(@query, %{}, "sessionState") + assert results["isValid"] == false + assert results["user"] == nil + end + + test "login user should get true sessionState", ~m(user)a do + user_conn = simu_conn(:user, user) + results = user_conn |> query_result(@query, %{}, "sessionState") + + assert results["isValid"] == true + assert results["user"] |> Map.get("id") == to_string(user.id) + end + + test "user with invalid token get false sessionState" do + user_conn = simu_conn(:invalid_token) + results = user_conn |> query_result(@query, %{}, "sessionState") + + assert results["isValid"] == false + assert results["user"] == nil + end + end end diff --git a/test/mastani_server_web/query/accounts/achievement_test.exs b/test/mastani_server_web/query/accounts/achievement_test.exs index 436ffcfe8..cb1432e8f 100644 --- a/test/mastani_server_web/query/accounts/achievement_test.exs +++ b/test/mastani_server_web/query/accounts/achievement_test.exs @@ -59,7 +59,7 @@ defmodule MastaniServer.Test.Query.Account.Achievement do @query """ query { - account { + user { id editableCommunities { entries { @@ -84,7 +84,7 @@ defmodule MastaniServer.Test.Query.Account.Achievement do {:ok, _} = CMS.set_editor(community2, title, user) variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "account") + results = user_conn |> query_result(@query, variables, "user") editable_communities = results["editableCommunities"] assert editable_communities["totalCount"] == 2 diff --git a/test/mastani_server_web/query/accounts/favorited_jobs_test.exs b/test/mastani_server_web/query/accounts/favorited_jobs_test.exs index ce4008ed8..7a63947d4 100644 --- a/test/mastani_server_web/query/accounts/favorited_jobs_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_jobs_test.exs @@ -18,7 +18,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do describe "[accounts favorited jobs]" do @query """ query($filter: PagedFilter!) { - account { + user { id favoritedJobs(filter: $filter) { entries { @@ -38,7 +38,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedJobs do random_id = jobs |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "account") + results = user_conn |> query_result(@query, variables, "user") # IO.inspect results, label: "hello" assert results["favoritedJobs"] |> Map.get("totalCount") == @total_count assert results["favoritedJobsCount"] == @total_count diff --git a/test/mastani_server_web/query/accounts/favorited_posts_test.exs b/test/mastani_server_web/query/accounts/favorited_posts_test.exs index 815c748e2..bd80d3ce5 100644 --- a/test/mastani_server_web/query/accounts/favorited_posts_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_posts_test.exs @@ -19,7 +19,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedPosts do describe "[account favorited posts]" do @query """ query($filter: PagedFilter!) { - account { + user { id favoritedPosts(filter: $filter) { entries { @@ -39,7 +39,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedPosts do random_id = posts |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "account") + results = user_conn |> query_result(@query, variables, "user") assert results["favoritedPosts"] |> Map.get("totalCount") == @total_count assert results["favoritedPostsCount"] == @total_count diff --git a/test/mastani_server_web/query/accounts/favorited_repos_test.exs b/test/mastani_server_web/query/accounts/favorited_repos_test.exs index 37bd31ffa..a30f99c53 100644 --- a/test/mastani_server_web/query/accounts/favorited_repos_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_repos_test.exs @@ -18,7 +18,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedRepos do describe "[accounts favorited repos]" do @query """ query($filter: PagedFilter!) { - account { + user { id favoritedRepos(filter: $filter) { entries { @@ -38,7 +38,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedRepos do random_id = repos |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "account") + results = user_conn |> query_result(@query, variables, "user") assert results["favoritedRepos"] |> Map.get("totalCount") == @total_count assert results["favoritedReposCount"] == @total_count diff --git a/test/mastani_server_web/query/accounts/favorited_videos_test.exs b/test/mastani_server_web/query/accounts/favorited_videos_test.exs index a55f87bc4..315b59f41 100644 --- a/test/mastani_server_web/query/accounts/favorited_videos_test.exs +++ b/test/mastani_server_web/query/accounts/favorited_videos_test.exs @@ -19,7 +19,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedVideos do describe "[account favorited videos]" do @query """ query($filter: PagedFilter!) { - account { + user { id favoritedVideos(filter: $filter) { entries { @@ -39,7 +39,7 @@ defmodule MastaniServer.Test.Query.Accounts.FavritedVideos do random_id = videos |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "account") + results = user_conn |> query_result(@query, variables, "user") assert results["favoritedVideos"] |> Map.get("totalCount") == @total_count assert results["favoritedVideosCount"] == @total_count diff --git a/test/mastani_server_web/query/accounts/messages_test.exs b/test/mastani_server_web/query/accounts/messages_test.exs index 1c2a2f873..27b5685e1 100644 --- a/test/mastani_server_web/query/accounts/messages_test.exs +++ b/test/mastani_server_web/query/accounts/messages_test.exs @@ -14,7 +14,7 @@ defmodule MastaniServer.Test.Query.Accounts.Messages do describe "[account messages queries]" do @query """ query { - account { + user { id mailBox { hasMail @@ -29,7 +29,7 @@ defmodule MastaniServer.Test.Query.Accounts.Messages do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) - result = user_conn |> query_result(@query, %{}, "account") + result = user_conn |> query_result(@query, %{}, "user") mail_box = result["mailBox"] assert mail_box["hasMail"] == false assert mail_box["totalCount"] == 0 @@ -39,7 +39,7 @@ defmodule MastaniServer.Test.Query.Accounts.Messages do mock_mentions_for(user, 2) mock_notifications_for(user, 18) - result = user_conn |> query_result(@query, %{}, "account") + result = user_conn |> query_result(@query, %{}, "user") mail_box = result["mailBox"] assert mail_box["hasMail"] == true assert mail_box["totalCount"] == 20 @@ -55,7 +55,7 @@ defmodule MastaniServer.Test.Query.Accounts.Messages do @query """ query($filter: MessagesFilter!) { - account { + user { id mentions(filter: $filter) { entries { @@ -90,14 +90,14 @@ defmodule MastaniServer.Test.Query.Accounts.Messages do user_conn = simu_conn(:user, user) variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@query, variables, "account") + result = user_conn |> query_result(@query, variables, "user") mentions = result["mentions"] assert mentions["totalCount"] == 0 mock_mentions_for(user, 3) variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@query, variables, "account") + result = user_conn |> query_result(@query, variables, "user") mentions = result["mentions"] assert mentions["totalCount"] == 3 @@ -109,14 +109,14 @@ defmodule MastaniServer.Test.Query.Accounts.Messages do user_conn = simu_conn(:user, user) variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@query, variables, "account") + result = user_conn |> query_result(@query, variables, "user") notifications = result["notifications"] assert notifications["totalCount"] == 0 mock_notifications_for(user, 3) variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@query, variables, "account") + result = user_conn |> query_result(@query, variables, "user") notifications = result["notifications"] assert notifications["totalCount"] == 3 @@ -128,14 +128,14 @@ defmodule MastaniServer.Test.Query.Accounts.Messages do user_conn = simu_conn(:user, user) variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@query, variables, "account") + result = user_conn |> query_result(@query, variables, "user") notifications = result["sysNotifications"] assert notifications["totalCount"] == 0 mock_sys_notification(5) variables = %{filter: %{page: 1, size: 20, read: false}} - result = user_conn |> query_result(@query, variables, "account") + result = user_conn |> query_result(@query, variables, "user") notifications = result["sysNotifications"] assert notifications["totalCount"] == 5 diff --git a/test/mastani_server_web/query/accounts/stared_contents_test.exs b/test/mastani_server_web/query/accounts/stared_contents_test.exs index 7bd86b038..646d1d07f 100644 --- a/test/mastani_server_web/query/accounts/stared_contents_test.exs +++ b/test/mastani_server_web/query/accounts/stared_contents_test.exs @@ -20,7 +20,7 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do describe "[accounts stared posts]" do @query """ query($filter: PagedFilter!) { - account { + user { id staredPosts(filter: $filter) { entries { @@ -40,7 +40,7 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do random_id = posts |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "account") + results = user_conn |> query_result(@query, variables, "user") # IO.inspect results, label: "hello" assert results["staredPosts"] |> Map.get("totalCount") == @total_count assert results["staredPostsCount"] == @total_count @@ -80,7 +80,7 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do describe "[accounts stared jobs]" do @query """ query($filter: PagedFilter!) { - account { + user { id staredJobs(filter: $filter) { entries { @@ -100,7 +100,7 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do random_id = jobs |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "account") + results = user_conn |> query_result(@query, variables, "user") # IO.inspect results, label: "hello" assert results["staredJobs"] |> Map.get("totalCount") == @total_count assert results["staredJobsCount"] == @total_count @@ -140,7 +140,7 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do describe "[accounts stared videos]" do @query """ query($filter: PagedFilter!) { - account { + user { id staredVideos(filter: $filter) { entries { @@ -160,7 +160,7 @@ defmodule MastaniServer.Test.Query.Accounts.StaredContents do random_id = videos |> Enum.shuffle() |> List.first() |> Map.get(:id) |> to_string variables = %{filter: %{page: 1, size: 20}} - results = user_conn |> query_result(@query, variables, "account") + results = user_conn |> query_result(@query, variables, "user") # IO.inspect results, label: "hello" assert results["staredVideos"] |> Map.get("totalCount") == @total_count assert results["staredVideosCount"] == @total_count From 87d00c79331f305b852c48f115e95833cc45cf0f Mon Sep 17 00:00:00 2001 From: mydearxym Date: Thu, 1 Nov 2018 23:29:25 +0800 Subject: [PATCH 119/129] fix(subscribed community): mismatch cur_user --- lib/mastani_server_web/resolvers/accounts_resolver.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 33d5e9b26..52af8ae7b 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -190,7 +190,7 @@ defmodule MastaniServerWeb.Resolvers.Accounts do end # for user self's - def subscribed_communities(_root, %{filter: filter}, %{cur_user: cur_user}) do + def subscribed_communities(_root, %{filter: filter}, %{context: %{cur_user: cur_user}}) do Accounts.subscribed_communities(%User{id: cur_user.id}, filter) end From 6b938c67d5620781f0df498f3d1fc0a1ea4b6b0e Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 2 Nov 2018 11:33:33 +0800 Subject: [PATCH 120/129] fix: remote ip mismatch --- lib/mastani_server/cms/delegates/community_operation.ex | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/mastani_server/cms/delegates/community_operation.ex b/lib/mastani_server/cms/delegates/community_operation.ex index 03e5fa009..40f62082d 100644 --- a/lib/mastani_server/cms/delegates/community_operation.ex +++ b/lib/mastani_server/cms/delegates/community_operation.ex @@ -102,8 +102,10 @@ defmodule MastaniServer.CMS.Delegate.CommunityOperation do def subscribe_community( %Community{id: community_id}, %User{id: user_id}, - remote_ip \\ "127.0.0.1" + remote_ip \\ {127, 0, 0, 1} ) do + remote_ip = Enum.join(Tuple.to_list(remote_ip), ".") + with {:ok, record} <- CommunitySubscriber |> ORM.create(~m(user_id community_id)a) do update_geo_info(community_id, user_id, remote_ip, :inc) Community |> ORM.find(record.community_id) @@ -113,8 +115,10 @@ defmodule MastaniServer.CMS.Delegate.CommunityOperation do def unsubscribe_community( %Community{id: community_id}, %User{id: user_id}, - remote_ip \\ "127.0.0.1" + remote_ip \\ {127, 0, 0, 1} ) do + remote_ip = Enum.join(Tuple.to_list(remote_ip), ".") + with {:ok, record} <- CommunitySubscriber |> ORM.findby_delete(community_id: community_id, user_id: user_id) do update_geo_info(community_id, user_id, remote_ip, :dec) From 954381fd7f072472678b954b6b6a22e3527c5c9a Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 2 Nov 2018 13:26:57 +0800 Subject: [PATCH 121/129] feat(content counts): add & refactor cms contents count add jobs/video/repo count --- lib/mastani_server/cms/utils/loader.ex | 31 ++++- .../schema/cms/cms_types.ex | 22 ++-- lib/mastani_server_web/schema/utils/helper.ex | 23 +++- .../query/cms/cms_counts_test.exs | 124 ++++++++++++++++++ 4 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 test/mastani_server_web/query/cms/cms_counts_test.exs diff --git a/lib/mastani_server/cms/utils/loader.ex b/lib/mastani_server/cms/utils/loader.ex index 35e060f87..9d868efc2 100644 --- a/lib/mastani_server/cms/utils/loader.ex +++ b/lib/mastani_server/cms/utils/loader.ex @@ -7,6 +7,8 @@ defmodule MastaniServer.CMS.Utils.Loader do alias Helper.QueryBuilder alias MastaniServer.Repo # alias MastaniServer.Accounts + alias MastaniServer.CMS.Repo, as: CMSRepo + alias MastaniServer.CMS.{ Author, CommunityEditor, @@ -22,7 +24,7 @@ defmodule MastaniServer.CMS.Utils.Loader do PostFavorite, PostStar, # JOB - # Job, + Job, JobViewer, JobFavorite, # JobStar, @@ -31,6 +33,7 @@ defmodule MastaniServer.CMS.Utils.Loader do JobCommentDislike, JobCommentLike, # Video + Video, VideoViewer, VideoFavorite, VideoStar, @@ -38,7 +41,7 @@ defmodule MastaniServer.CMS.Utils.Loader do VideoCommentReply, VideoCommentDislike, VideoCommentLike, - # repo + # Repo, RepoViewer, RepoFavorite, RepoComment, @@ -51,14 +54,30 @@ defmodule MastaniServer.CMS.Utils.Loader do # Big thanks: https://elixirforum.com/t/grouping-error-in-absinthe-dadaloader/13671/2 # see also: https://github.com/absinthe-graphql/dataloader/issues/25 - def run_batch(Post, post_query, :posts_count, community_ids, repo_opts) do + def run_batch(Post, content_query, :posts_count, community_ids, repo_opts) do + query_content_counts(content_query, community_ids, repo_opts) + end + + def run_batch(Job, content_query, :jobs_count, community_ids, repo_opts) do + query_content_counts(content_query, community_ids, repo_opts) + end + + def run_batch(Video, content_query, :videos_count, community_ids, repo_opts) do + query_content_counts(content_query, community_ids, repo_opts) + end + + def run_batch(CMSRepo, content_query, :repos_count, community_ids, repo_opts) do + query_content_counts(content_query, community_ids, repo_opts) + end + + defp query_content_counts(content_query, community_ids, repo_opts) do query = from( - p in post_query, - join: c in assoc(p, :communities), + content in content_query, + join: c in assoc(content, :communities), where: c.id in ^community_ids, group_by: c.id, - select: {c.id, [count(p.id)]} + select: {c.id, [count(content.id)]} ) results = diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 678b39a16..54405623d 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -258,17 +258,17 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:threads, list_of(:thread), resolve: dataloader(CMS, :threads)) field(:categories, list_of(:category), resolve: dataloader(CMS, :categories)) - # Big thanks: https://elixirforum.com/t/grouping-error-in-absinthe-dadaloader/13671/2 - # see also: https://github.com/absinthe-graphql/dataloader/issues/25 - field :posts_count, :integer do - resolve(fn community, _args, %{context: %{loader: loader}} -> - loader - |> Dataloader.load(CMS, {:one, CMS.Post}, posts_count: community.id) - |> on_load(fn loader -> - {:ok, Dataloader.get(loader, CMS, {:one, CMS.Post}, posts_count: community.id)} - end) - end) - end + @doc "total count of post contents" + content_counts_field(:post, CMS.Post) + + @doc "total count of job contents" + content_counts_field(:job, CMS.Job) + + @doc "total count of video contents" + content_counts_field(:video, CMS.Video) + + @doc "total count of repo contents" + content_counts_field(:repo, CMS.Repo) field :subscribers, list_of(:user) do arg(:filter, :members_filter) diff --git a/lib/mastani_server_web/schema/utils/helper.ex b/lib/mastani_server_web/schema/utils/helper.ex index 2b1e88818..088b46ce4 100644 --- a/lib/mastani_server_web/schema/utils/helper.ex +++ b/lib/mastani_server_web/schema/utils/helper.ex @@ -50,8 +50,29 @@ defmodule MastaniServerWeb.Schema.Utils.Helper do import Absinthe.Resolution.Helpers, only: [dataloader: 2] alias MastaniServer.CMS - alias MastaniServerWeb.Resolvers, as: R alias MastaniServerWeb.Middleware, as: M + alias MastaniServerWeb.Resolvers, as: R + + # Big thanks: https://elixirforum.com/t/grouping-error-in-absinthe-dadaloader/13671/2 + # see also: https://github.com/absinthe-graphql/dataloader/issues/25 + defmacro content_counts_field(thread, schema) do + quote do + field unquote(String.to_atom("#{to_string(thread)}s_count")), :integer do + resolve(fn community, _args, %{context: %{loader: loader}} -> + loader + |> Dataloader.load(CMS, {:one, unquote(schema)}, [ + {unquote(String.to_atom("#{to_string(thread)}s_count")), community.id} + ]) + |> on_load(fn loader -> + {:ok, + Dataloader.get(loader, CMS, {:one, unquote(schema)}, [ + {unquote(String.to_atom("#{to_string(thread)}s_count")), community.id} + ])} + end) + end) + end + end + end defmacro has_viewed_field do quote do diff --git a/test/mastani_server_web/query/cms/cms_counts_test.exs b/test/mastani_server_web/query/cms/cms_counts_test.exs new file mode 100644 index 000000000..00112a116 --- /dev/null +++ b/test/mastani_server_web/query/cms/cms_counts_test.exs @@ -0,0 +1,124 @@ +defmodule MastaniServer.Test.Query.CMS.ContentCounts do + use MastaniServer.TestTools + + # alias MastaniServer.Accounts.User + alias MastaniServer.CMS + # alias CMS.{Community, Thread, Category} + + setup do + guest_conn = simu_conn(:guest) + {:ok, community} = db_insert(:community) + {:ok, user} = db_insert(:user) + + {:ok, ~m(guest_conn community user)a} + end + + describe "[cms contents count]" do + @query """ + query($id: ID) { + community(id: $id) { + id + title + postsCount + } + } + """ + test "community have valid posts_count", ~m(guest_conn community user)a do + variables = %{id: community.id} + results = guest_conn |> query_result(@query, variables, "community") + assert results["postsCount"] == 0 + + count = Enum.random(1..20) + + Enum.reduce(1..count, [], fn _, acc -> + post_attrs = mock_attrs(:post, %{community_id: community.id}) + {:ok, post} = CMS.create_content(community, :post, post_attrs, user) + acc ++ [post] + end) + + results = guest_conn |> query_result(@query, variables, "community") + assert results["postsCount"] == count + end + + @query """ + query($id: ID) { + community(id: $id) { + id + title + jobsCount + } + } + """ + test "community have valid jobs_count", ~m(guest_conn community user)a do + variables = %{id: community.id} + results = guest_conn |> query_result(@query, variables, "community") + assert results["jobsCount"] == 0 + + count = Enum.random(1..20) + + Enum.reduce(1..count, [], fn _, acc -> + job_attrs = mock_attrs(:job, %{community_id: community.id}) + {:ok, job} = CMS.create_content(community, :job, job_attrs, user) + + acc ++ [job] + end) + + results = guest_conn |> query_result(@query, variables, "community") + assert results["jobsCount"] == count + end + + @query """ + query($id: ID) { + community(id: $id) { + id + title + videosCount + } + } + """ + test "community have valid videos_count", ~m(guest_conn community user)a do + variables = %{id: community.id} + results = guest_conn |> query_result(@query, variables, "community") + assert results["videosCount"] == 0 + + count = Enum.random(1..20) + + Enum.reduce(1..count, [], fn _, acc -> + video_attrs = mock_attrs(:video, %{community_id: community.id}) + {:ok, video} = CMS.create_content(community, :video, video_attrs, user) + + acc ++ [video] + end) + + results = guest_conn |> query_result(@query, variables, "community") + assert results["videosCount"] == count + end + + @query """ + query($id: ID) { + community(id: $id) { + id + title + reposCount + } + } + """ + test "community have valid repos_count", ~m(guest_conn community user)a do + variables = %{id: community.id} + results = guest_conn |> query_result(@query, variables, "community") + assert results["reposCount"] == 0 + + count = Enum.random(1..20) + + Enum.reduce(1..count, [], fn _, acc -> + repo_attrs = mock_attrs(:repo, %{community_id: community.id}) + {:ok, repo} = CMS.create_content(community, :repo, repo_attrs, user) + + acc ++ [repo] + end) + + results = guest_conn |> query_result(@query, variables, "community") + assert results["reposCount"] == count + end + end +end From 2858be79f060a6fb613c7673656ff3126c3b545f Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 2 Nov 2018 17:50:33 +0800 Subject: [PATCH 122/129] chore(clean up): warnings --- test/mastani_server_web/query/cms/posts_topic_test.exs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/mastani_server_web/query/cms/posts_topic_test.exs b/test/mastani_server_web/query/cms/posts_topic_test.exs index 1a6e1536d..09597adb7 100644 --- a/test/mastani_server_web/query/cms/posts_topic_test.exs +++ b/test/mastani_server_web/query/cms/posts_topic_test.exs @@ -1,8 +1,6 @@ defmodule MastaniServer.Test.Query.PostsTopic do use MastaniServer.TestTools - # import Helper.Utils, only: [get_config: 2] - alias Helper.ORM alias MastaniServer.CMS setup do @@ -63,7 +61,7 @@ defmodule MastaniServer.Test.Query.PostsTopic do post_attr = mock_attrs(:post) variables = post_attr |> Map.merge(%{communityId: community.id, topic: "city"}) - created = user_conn |> mutation_result(@create_post_query, variables, "createPost") + _created = user_conn |> mutation_result(@create_post_query, variables, "createPost") variables = %{filter: %{page: 1, size: 10, topic: "city"}} results = guest_conn |> query_result(@query, variables, "pagedPosts") From eb29d676c382c07ba03cb6c4368af380fe4938f1 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Fri, 2 Nov 2018 23:22:25 +0800 Subject: [PATCH 123/129] refactor(pin contents): use seperate table to pin contens --- lib/helper/query_builder.ex | 7 ++ lib/mastani_server/cms/cms.ex | 8 ++ .../cms/delegates/article_curd.ex | 89 ++++++++++++++++-- .../cms/delegates/article_operation.ex | 94 ++++++++++++++++++- lib/mastani_server/cms/pined_job.ex | 28 ++++++ lib/mastani_server/cms/pined_post.ex | 30 ++++++ lib/mastani_server/cms/pined_repo.ex | 28 ++++++ lib/mastani_server/cms/pined_video.ex | 28 ++++++ .../resolvers/cms_resolver.ex | 46 ++++++++- .../schema/cms/cms_types.ex | 2 +- .../schema/cms/mutations/post.ex | 4 +- ...2103138_add_flag_index_to_cms_contents.exs | 10 ++ .../20181102103942_create_pined_posts.exs | 15 +++ .../20181102152301_create_pined_jobs.exs | 14 +++ .../20181102164008_create_pined_videos.exs | 14 +++ .../20181102165813_create_pined_repos.exs | 14 +++ test/mastani_server/cms/content_flag_test.exs | 28 +++--- test/mastani_server/cms/content_pin_test.exs | 93 ++++++++++++++++++ .../mutation/cms/job_flag_test.exs | 7 +- .../mutation/cms/post_flag_test.exs | 6 +- .../mutation/cms/repo_flag_test.exs | 6 +- .../mutation/cms/video_flag_test.exs | 7 +- .../query/cms/jobs_flags_test.exs | 9 +- .../query/cms/posts_flags_test.exs | 12 ++- .../query/cms/posts_topic_test.exs | 4 +- .../query/cms/repos_flags_test.exs | 10 +- .../query/cms/videos_flags_test.exs | 7 +- 27 files changed, 571 insertions(+), 49 deletions(-) create mode 100644 lib/mastani_server/cms/pined_job.ex create mode 100644 lib/mastani_server/cms/pined_post.ex create mode 100644 lib/mastani_server/cms/pined_repo.ex create mode 100644 lib/mastani_server/cms/pined_video.ex create mode 100644 priv/repo/migrations/20181102103138_add_flag_index_to_cms_contents.exs create mode 100644 priv/repo/migrations/20181102103942_create_pined_posts.exs create mode 100644 priv/repo/migrations/20181102152301_create_pined_jobs.exs create mode 100644 priv/repo/migrations/20181102164008_create_pined_videos.exs create mode 100644 priv/repo/migrations/20181102165813_create_pined_repos.exs create mode 100644 test/mastani_server/cms/content_pin_test.exs diff --git a/lib/helper/query_builder.ex b/lib/helper/query_builder.ex index 9682baa3d..7eeea0a16 100644 --- a/lib/helper/query_builder.ex +++ b/lib/helper/query_builder.ex @@ -206,6 +206,13 @@ defmodule Helper.QueryBuilder do where: t.raw == ^community_raw ) + {:one_community, community_raw}, queryable -> + from( + q in queryable, + join: t in assoc(q, :community), + where: t.raw == ^community_raw + ) + {:first, first}, queryable -> queryable |> limit(^first) diff --git a/lib/mastani_server/cms/cms.ex b/lib/mastani_server/cms/cms.ex index 3eb3a4be8..c1b3bd5b3 100644 --- a/lib/mastani_server/cms/cms.ex +++ b/lib/mastani_server/cms/cms.ex @@ -74,6 +74,14 @@ defmodule MastaniServer.CMS do # ArticleOperation # >> set flag on article, like: pin / unpin article defdelegate set_community_flags(queryable, community_id, attrs), to: ArticleOperation + defdelegate pin_content(queryable, community_id, topic), to: ArticleOperation + defdelegate undo_pin_content(queryable, community_id, topic), to: ArticleOperation + defdelegate pin_content(queryable, community_id), to: ArticleOperation + defdelegate undo_pin_content(queryable, community_id), to: ArticleOperation + + # defdelegate pin_content(queryable, community_id, thread), to: ArticleOperation + # defdelegate undo_pin_content(queryable, community_id, thread, topic), to: ArticleOperation + # defdelegate undo_pin_content(queryable, community_id, thread), to: ArticleOperation # >> tag: set / unset defdelegate set_tag(community, thread, tag, content_id), to: ArticleOperation diff --git a/lib/mastani_server/cms/delegates/article_curd.ex b/lib/mastani_server/cms/delegates/article_curd.ex index 14daa0e75..4b499d20a 100644 --- a/lib/mastani_server/cms/delegates/article_curd.ex +++ b/lib/mastani_server/cms/delegates/article_curd.ex @@ -47,14 +47,14 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do end defp flag_query(queryable, filter, flag \\ %{}) do - flag = %{pin: false, trash: false} |> Map.merge(flag) + flag = %{trash: false} |> Map.merge(flag) # NOTE: this case judge is used for test case case filter |> Map.has_key?(:community) do true -> queryable |> join(:inner, [q], f in assoc(q, :community_flags)) - |> where([q, f], f.pin == ^flag.pin and f.trash == ^flag.trash) + |> where([q, f], f.trash == ^flag.trash) |> join(:inner, [q, f], c in assoc(f, :community)) |> where([q, f, c], c.raw == ^filter.community) @@ -64,17 +64,67 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do end # only first page need pin contents - defp add_pin_contents_ifneed(contents, queryable, filter) do + # TODO: use seperate pined table, which is much more smaller + defp add_pin_contents_ifneed(contents, CMS.Post, %{community: community} = filter) do with {:ok, normal_contents} <- contents, true <- Map.has_key?(filter, :community), true <- 1 == Map.get(normal_contents, :page_number) do {:ok, pined_content} = - queryable - |> flag_query(filter, %{pin: true}) - |> ORM.find_all(filter) + CMS.PinedPost + |> join(:inner, [p], c in assoc(p, :community)) + |> join(:inner, [p], t in assoc(p, :topic)) + |> join(:inner, [p], content in assoc(p, :post)) + |> where( + [p, c, t, content], + c.raw == ^filter.community and t.raw == ^Map.get(filter, :topic, "posts") + ) + |> select([p, c, t, content], content) + # 10 pined contents per community/thread, at most + |> ORM.paginater(%{page: 1, size: 10}) + |> done() + + concat_contents(pined_content, normal_contents) + else + _error -> + contents + end + end + + defp add_pin_contents_ifneed(contents, CMS.Job, %{community: community} = filter) do + with {:ok, normal_contents} <- contents, + true <- Map.has_key?(filter, :community), + true <- 1 == Map.get(normal_contents, :page_number) do + {:ok, pined_content} = + CMS.PinedJob + |> join(:inner, [p], c in assoc(p, :community)) + |> join(:inner, [p], content in assoc(p, :job)) + |> where([p, c, content], c.raw == ^filter.community) + |> select([p, c, content], content) + # 10 pined contents per community/thread, at most + |> ORM.paginater(%{page: 1, size: 10}) + |> done() + + concat_contents(pined_content, normal_contents) + else + _error -> + contents + end + end + + defp add_pin_contents_ifneed(contents, CMS.Video, %{community: community} = filter) do + with {:ok, normal_contents} <- contents, + true <- Map.has_key?(filter, :community), + true <- 1 == Map.get(normal_contents, :page_number) do + {:ok, pined_content} = + CMS.PinedVideo + |> join(:inner, [p], c in assoc(p, :community)) + |> join(:inner, [p], content in assoc(p, :video)) + |> where([p, c, content], c.raw == ^filter.community) + |> select([p, c, content], content) + # 10 pined contents per community/thread, at most + |> ORM.paginater(%{page: 1, size: 10}) + |> done() - # TODO: add hot post pin/trash state ? - # don't by flag_changeset, dataloader make things complex concat_contents(pined_content, normal_contents) else _error -> @@ -82,6 +132,29 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do end end + defp add_pin_contents_ifneed(contents, CMS.Repo, %{community: community} = filter) do + with {:ok, normal_contents} <- contents, + true <- Map.has_key?(filter, :community), + true <- 1 == Map.get(normal_contents, :page_number) do + {:ok, pined_content} = + CMS.PinedRepo + |> join(:inner, [p], c in assoc(p, :community)) + |> join(:inner, [p], content in assoc(p, :repo)) + |> where([p, c, content], c.raw == ^filter.community) + |> select([p, c, content], content) + # 10 pined contents per community/thread, at most + |> ORM.paginater(%{page: 1, size: 10}) + |> done() + + concat_contents(pined_content, normal_contents) + else + _error -> + contents + end + end + + defp add_pin_contents_ifneed(contents, _querable, _filter), do: contents + defp concat_contents(pined_content, normal_contents) do case pined_content |> Map.get(:total_count) do 0 -> diff --git a/lib/mastani_server/cms/delegates/article_operation.ex b/lib/mastani_server/cms/delegates/article_operation.ex index b71a3e40d..79e3c023d 100644 --- a/lib/mastani_server/cms/delegates/article_operation.ex +++ b/lib/mastani_server/cms/delegates/article_operation.ex @@ -19,14 +19,104 @@ defmodule MastaniServer.CMS.Delegate.ArticleOperation do Video, VideoCommunityFlag, Tag, - Topic + Topic, + PinedPost, + PinedJob, + PinedVideo, + PinedRepo } alias MastaniServer.CMS.Repo, as: CMSRepo alias MastaniServer.Repo + def pin_content(%Post{id: post_id}, %Community{id: community_id}, topic) do + with {:ok, %{id: topic_id}} <- ORM.find_by(Topic, %{raw: topic}), + {:ok, pined} <- + ORM.findby_or_insert( + PinedPost, + ~m(post_id community_id topic_id)a, + ~m(post_id community_id topic_id)a + ) do + Post |> ORM.find(pined.post_id) + end + end + + def undo_pin_content(%Post{id: post_id}, %Community{id: community_id}, topic) do + with {:ok, %{id: topic_id}} <- ORM.find_by(Topic, %{raw: topic}), + {:ok, pined} <- + ORM.find_by( + PinedPost, + post_id: post_id, + community_id: community_id, + topic_id: topic_id + ), + {:ok, deleted} <- ORM.delete(pined) do + Post |> ORM.find(deleted.post_id) + end + end + + def pin_content(%Job{id: job_id}, %Community{id: community_id}) do + attrs = ~m(job_id community_id)a + + with {:ok, pined} <- ORM.findby_or_insert(PinedJob, attrs, attrs) do + Job |> ORM.find(pined.job_id) + end + end + + def undo_pin_content(%Job{id: job_id}, %Community{id: community_id}) do + with {:ok, pined} <- + ORM.find_by( + PinedJob, + job_id: job_id, + community_id: community_id + ), + {:ok, deleted} <- ORM.delete(pined) do + Job |> ORM.find(deleted.job_id) + end + end + + def pin_content(%Video{id: video_id}, %Community{id: community_id}) do + attrs = ~m(video_id community_id)a + + with {:ok, pined} <- ORM.findby_or_insert(PinedVideo, attrs, attrs) do + Video |> ORM.find(pined.video_id) + end + end + + def undo_pin_content(%Video{id: video_id}, %Community{id: community_id}) do + with {:ok, pined} <- + ORM.find_by( + PinedVideo, + video_id: video_id, + community_id: community_id + ), + {:ok, deleted} <- ORM.delete(pined) do + Video |> ORM.find(deleted.video_id) + end + end + + def pin_content(%CMSRepo{id: repo_id}, %Community{id: community_id}) do + attrs = ~m(repo_id community_id)a + + with {:ok, pined} <- ORM.findby_or_insert(PinedRepo, attrs, attrs) do + CMSRepo |> ORM.find(pined.repo_id) + end + end + + def undo_pin_content(%CMSRepo{id: repo_id}, %Community{id: community_id}) do + with {:ok, pined} <- + ORM.find_by( + PinedRepo, + repo_id: repo_id, + community_id: community_id + ), + {:ok, deleted} <- ORM.delete(pined) do + CMSRepo |> ORM.find(deleted.repo_id) + end + end + @doc """ - pin / unpin, trash / untrash articles + trash / untrash articles """ def set_community_flags(%Post{id: _} = content, community_id, attrs), do: do_set_flag(content, community_id, attrs) diff --git a/lib/mastani_server/cms/pined_job.ex b/lib/mastani_server/cms/pined_job.ex new file mode 100644 index 000000000..8495ae3b3 --- /dev/null +++ b/lib/mastani_server/cms/pined_job.ex @@ -0,0 +1,28 @@ +defmodule MastaniServer.CMS.PinedJob do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.CMS.{Community, Job} + + @required_fields ~w(job_id community_id)a + + @type t :: %PinedJob{} + schema "pined_jobs" do + belongs_to(:job, Job, foreign_key: :job_id) + belongs_to(:community, Community, foreign_key: :community_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%PinedJob{} = pined_job, attrs) do + pined_job + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:job_id) + |> foreign_key_constraint(:community_id) + |> unique_constraint(:pined_jobs, name: :pined_jobs_job_id_community_id_index) + end +end diff --git a/lib/mastani_server/cms/pined_post.ex b/lib/mastani_server/cms/pined_post.ex new file mode 100644 index 000000000..fdc0d8a37 --- /dev/null +++ b/lib/mastani_server/cms/pined_post.ex @@ -0,0 +1,30 @@ +defmodule MastaniServer.CMS.PinedPost do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.CMS.{Community, Post, Topic} + + @required_fields ~w(post_id community_id)a + @optional_fields ~w(topic_id)a + + @type t :: %PinedPost{} + schema "pined_posts" do + belongs_to(:post, Post, foreign_key: :post_id) + belongs_to(:community, Community, foreign_key: :community_id) + belongs_to(:topic, Topic, foreign_key: :topic_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%PinedPost{} = pined_post, attrs) do + pined_post + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:post_id) + |> foreign_key_constraint(:community_id) + |> unique_constraint(:pined_posts, name: :pined_posts_post_id_community_id_topic_id_index) + end +end diff --git a/lib/mastani_server/cms/pined_repo.ex b/lib/mastani_server/cms/pined_repo.ex new file mode 100644 index 000000000..9d7465b01 --- /dev/null +++ b/lib/mastani_server/cms/pined_repo.ex @@ -0,0 +1,28 @@ +defmodule MastaniServer.CMS.PinedRepo do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.CMS.{Community, Repo} + + @required_fields ~w(repo_id community_id)a + + @type t :: %PinedRepo{} + schema "pined_repos" do + belongs_to(:repo, Repo, foreign_key: :repo_id) + belongs_to(:community, Community, foreign_key: :community_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%PinedRepo{} = pined_repo, attrs) do + pined_repo + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:repo_id) + |> foreign_key_constraint(:community_id) + |> unique_constraint(:pined_repos, name: :pined_repos_repo_id_community_id_index) + end +end diff --git a/lib/mastani_server/cms/pined_video.ex b/lib/mastani_server/cms/pined_video.ex new file mode 100644 index 000000000..a4693448a --- /dev/null +++ b/lib/mastani_server/cms/pined_video.ex @@ -0,0 +1,28 @@ +defmodule MastaniServer.CMS.PinedVideo do + @moduledoc false + alias __MODULE__ + + use Ecto.Schema + import Ecto.Changeset + alias MastaniServer.CMS.{Community, Video} + + @required_fields ~w(video_id community_id)a + + @type t :: %PinedVideo{} + schema "pined_videos" do + belongs_to(:video, Video, foreign_key: :video_id) + belongs_to(:community, Community, foreign_key: :community_id) + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(%PinedVideo{} = pined_video, attrs) do + pined_video + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:video_id) + |> foreign_key_constraint(:community_id) + |> unique_constraint(:pined_videos, name: :pined_videos_video_id_community_id_index) + end +end diff --git a/lib/mastani_server_web/resolvers/cms_resolver.ex b/lib/mastani_server_web/resolvers/cms_resolver.ex index ec1935219..4be8c03ea 100644 --- a/lib/mastani_server_web/resolvers/cms_resolver.ex +++ b/lib/mastani_server_web/resolvers/cms_resolver.ex @@ -77,11 +77,42 @@ defmodule MastaniServerWeb.Resolvers.CMS do # ####################### # content flag .. # ####################### - def pin_content(_root, ~m(id thread community_id)a, _info), - do: set_community_flags(community_id, thread, id, %{pin: true}) + def pin_content(_root, ~m(id community_id thread topic)a, _info) do + CMS.pin_content(%CMS.Post{id: id}, %Community{id: community_id}, topic) + end + + def undo_pin_content(_root, ~m(id community_id thread topic)a, _info) do + CMS.undo_pin_content(%CMS.Post{id: id}, %Community{id: community_id}, topic) + end + + def pin_content(_root, ~m(id community_id thread)a, _info) do + do_pin_content(id, community_id, thread) + end + + def undo_pin_content(_root, ~m(id community_id thread)a, _info) do + do_undo_pin_content(id, community_id, thread) + end + + def do_pin_content(id, community_id, :job), + do: CMS.pin_content(%CMS.Job{id: id}, %Community{id: community_id}) + + def do_pin_content(id, community_id, :video), + do: CMS.pin_content(%CMS.Video{id: id}, %Community{id: community_id}) - def undo_pin_content(_root, ~m(id thread community_id)a, _info), - do: set_community_flags(community_id, thread, id, %{pin: false}) + def do_pin_content(id, community_id, :repo), + do: CMS.pin_content(%CMS.Repo{id: id}, %Community{id: community_id}) + + def do_undo_pin_content(id, community_id, :job) do + CMS.undo_pin_content(%CMS.Job{id: id}, %Community{id: community_id}) + end + + def do_undo_pin_content(id, community_id, :video) do + CMS.undo_pin_content(%CMS.Video{id: id}, %Community{id: community_id}) + end + + def do_undo_pin_content(id, community_id, :repo) do + CMS.undo_pin_content(%CMS.Repo{id: id}, %Community{id: community_id}) + end def trash_content(_root, ~m(id thread community_id)a, _info), do: set_community_flags(community_id, thread, id, %{trash: true}) @@ -89,6 +120,13 @@ defmodule MastaniServerWeb.Resolvers.CMS do def undo_trash_content(_root, ~m(id thread community_id)a, _info), do: set_community_flags(community_id, thread, id, %{trash: false}) + # TODO: report contents + # def report_content(_root, ~m(id thread community_id)a, _info), + # do: set_community_flags(community_id, thread, id, %{report: true}) + + # def undo_report_content(_root, ~m(id thread community_id)a, _info), + # do: set_community_flags(community_id, thread, id, %{report: false}) + defp set_community_flags(community_id, thread, id, flag) do with {:ok, content} <- match_action(thread, :self) do content.target diff --git a/lib/mastani_server_web/schema/cms/cms_types.ex b/lib/mastani_server_web/schema/cms/cms_types.ex index 54405623d..14cf5faa1 100644 --- a/lib/mastani_server_web/schema/cms/cms_types.ex +++ b/lib/mastani_server_web/schema/cms/cms_types.ex @@ -27,7 +27,7 @@ defmodule MastaniServerWeb.Schema.CMS.Types do field(:copy_right, :string) field(:body, :string) field(:views, :integer) - # TODO: remove + # NOTE: only meaningful in paged-xxx queries field(:pin, :boolean) field(:trash, :boolean) field(:tags, list_of(:tag), resolve: dataloader(CMS, :tags)) diff --git a/lib/mastani_server_web/schema/cms/mutations/post.ex b/lib/mastani_server_web/schema/cms/mutations/post.ex index 4a87301eb..03d945c26 100644 --- a/lib/mastani_server_web/schema/cms/mutations/post.ex +++ b/lib/mastani_server_web/schema/cms/mutations/post.ex @@ -27,8 +27,9 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Post do @desc "pin a post" field :pin_post, :post do arg(:id, non_null(:id)) - arg(:thread, :post_thread, default_value: :post) arg(:community_id, non_null(:id)) + arg(:thread, :post_thread, default_value: :post) + arg(:topic, :string, default_value: "posts") middleware(M.Authorize, :login) middleware(M.PassportLoader, source: :community) @@ -41,6 +42,7 @@ defmodule MastaniServerWeb.Schema.CMS.Mutations.Post do arg(:id, non_null(:id)) arg(:thread, :post_thread, default_value: :post) arg(:community_id, non_null(:id)) + arg(:topic, :string, default_value: "posts") middleware(M.Authorize, :login) middleware(M.PassportLoader, source: :community) diff --git a/priv/repo/migrations/20181102103138_add_flag_index_to_cms_contents.exs b/priv/repo/migrations/20181102103138_add_flag_index_to_cms_contents.exs new file mode 100644 index 000000000..c788796c3 --- /dev/null +++ b/priv/repo/migrations/20181102103138_add_flag_index_to_cms_contents.exs @@ -0,0 +1,10 @@ +defmodule MastaniServer.Repo.Migrations.AddFlagIndexToCmsContents do + use Ecto.Migration + + def change do + create(index(:posts_communities_flags, [:trash])) + create(index(:jobs_communities_flags, [:trash])) + create(index(:repos_communities_flags, [:trash])) + create(index(:videos_communities_flags, [:trash])) + end +end diff --git a/priv/repo/migrations/20181102103942_create_pined_posts.exs b/priv/repo/migrations/20181102103942_create_pined_posts.exs new file mode 100644 index 000000000..357ac158c --- /dev/null +++ b/priv/repo/migrations/20181102103942_create_pined_posts.exs @@ -0,0 +1,15 @@ +defmodule MastaniServer.Repo.Migrations.CreatePinedPosts do + use Ecto.Migration + + def change do + create table(:pined_posts) do + add(:post_id, references(:cms_posts, on_delete: :delete_all), null: false) + add(:community_id, references(:communities, on_delete: :delete_all), null: false) + add(:topic_id, references(:topics, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:pined_posts, [:post_id, :community_id, :topic_id])) + end +end diff --git a/priv/repo/migrations/20181102152301_create_pined_jobs.exs b/priv/repo/migrations/20181102152301_create_pined_jobs.exs new file mode 100644 index 000000000..e30a41d97 --- /dev/null +++ b/priv/repo/migrations/20181102152301_create_pined_jobs.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreatePinedJobs do + use Ecto.Migration + + def change do + create table(:pined_jobs) do + add(:job_id, references(:cms_jobs, on_delete: :delete_all), null: false) + add(:community_id, references(:communities, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:pined_jobs, [:job_id, :community_id])) + end +end diff --git a/priv/repo/migrations/20181102164008_create_pined_videos.exs b/priv/repo/migrations/20181102164008_create_pined_videos.exs new file mode 100644 index 000000000..8314f33c1 --- /dev/null +++ b/priv/repo/migrations/20181102164008_create_pined_videos.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreatePinedVideos do + use Ecto.Migration + + def change do + create table(:pined_videos) do + add(:video_id, references(:cms_videos, on_delete: :delete_all), null: false) + add(:community_id, references(:communities, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:pined_videos, [:video_id, :community_id])) + end +end diff --git a/priv/repo/migrations/20181102165813_create_pined_repos.exs b/priv/repo/migrations/20181102165813_create_pined_repos.exs new file mode 100644 index 000000000..29822cabb --- /dev/null +++ b/priv/repo/migrations/20181102165813_create_pined_repos.exs @@ -0,0 +1,14 @@ +defmodule MastaniServer.Repo.Migrations.CreatePinedRepos do + use Ecto.Migration + + def change do + create table(:pined_repos) do + add(:repo_id, references(:cms_repos, on_delete: :delete_all), null: false) + add(:community_id, references(:communities, on_delete: :delete_all), null: false) + + timestamps() + end + + create(unique_index(:pined_repos, [:repo_id, :community_id])) + end +end diff --git a/test/mastani_server/cms/content_flag_test.exs b/test/mastani_server/cms/content_flag_test.exs index 62e3cb481..d5292a099 100644 --- a/test/mastani_server/cms/content_flag_test.exs +++ b/test/mastani_server/cms/content_flag_test.exs @@ -29,18 +29,17 @@ defmodule MastaniServer.Test.ContentFlags do end describe "[cms post flag]" do - test "user can set pin/trash flag on post based on community", ~m(community post)a do + @tag :wip + test "user can set trash flag on post based on community", ~m(community post)a do community_id = community.id post_id = post.id {:ok, found} = PostCommunityFlag |> ORM.find_by(~m(post_id community_id)a) - assert found.pin == false assert found.trash == false - CMS.set_community_flags(%Post{id: post.id}, community.id, %{pin: true, trash: true}) + CMS.set_community_flags(%Post{id: post.id}, community.id, %{trash: true}) {:ok, found} = PostCommunityFlag |> ORM.find_by(~m(post_id community_id)a) - assert found.pin == true assert found.trash == true assert found.post_id == post.id assert found.community_id == community.id @@ -50,19 +49,18 @@ defmodule MastaniServer.Test.ContentFlags do end describe "[cms job flag]" do - test "user can set pin/trash flag on job", ~m(community job)a do + @tag :wip + test "user can set trash flag on job", ~m(community job)a do community_id = community.id job_id = job.id {:ok, found} = JobCommunityFlag |> ORM.find_by(~m(job_id community_id)a) - assert found.pin == false assert found.trash == false - CMS.set_community_flags(%Job{id: job.id}, community.id, %{pin: true, trash: true}) + CMS.set_community_flags(%Job{id: job.id}, community.id, %{trash: true}) {:ok, found} = JobCommunityFlag |> ORM.find_by(~m(job_id community_id)a) - assert found.pin == true assert found.trash == true assert found.job_id == job.id assert found.community_id == community.id @@ -70,19 +68,18 @@ defmodule MastaniServer.Test.ContentFlags do end describe "[cms video flag]" do - test "user can set pin/trash flag on a video", ~m(community video)a do + @tag :wip + test "user can set trash flag on a video", ~m(community video)a do community_id = community.id video_id = video.id {:ok, found} = VideoCommunityFlag |> ORM.find_by(~m(video_id community_id)a) - assert found.pin == false assert found.trash == false - CMS.set_community_flags(%Video{id: video.id}, community.id, %{pin: true, trash: true}) + CMS.set_community_flags(%Video{id: video.id}, community.id, %{trash: true}) {:ok, found} = VideoCommunityFlag |> ORM.find_by(~m(video_id community_id)a) - assert found.pin == true assert found.trash == true assert found.video_id == video.id assert found.community_id == community.id @@ -90,19 +87,18 @@ defmodule MastaniServer.Test.ContentFlags do end describe "[cms repo flag]" do - test "user can set pin/trash flag on repo", ~m(community repo)a do + @tag :wip + test "user can set trash flag on repo", ~m(community repo)a do community_id = community.id repo_id = repo.id {:ok, found} = RepoCommunityFlag |> ORM.find_by(~m(repo_id community_id)a) - assert found.pin == false assert found.trash == false - CMS.set_community_flags(%Repo{id: repo.id}, community.id, %{pin: true, trash: true}) + CMS.set_community_flags(%Repo{id: repo.id}, community.id, %{trash: true}) {:ok, found} = RepoCommunityFlag |> ORM.find_by(~m(repo_id community_id)a) - assert found.pin == true assert found.trash == true assert found.repo_id == repo.id assert found.community_id == community.id diff --git a/test/mastani_server/cms/content_pin_test.exs b/test/mastani_server/cms/content_pin_test.exs new file mode 100644 index 000000000..164a65ba2 --- /dev/null +++ b/test/mastani_server/cms/content_pin_test.exs @@ -0,0 +1,93 @@ +defmodule MastaniServer.Test.ContentsPin do + use MastaniServer.TestTools + + # alias Helper.ORM + alias MastaniServer.CMS + + # alias CMS.{ + # Post, + # PinedPost, + # Job, + # PinedJob, + # } + + setup do + {:ok, user} = db_insert(:user) + {:ok, community} = db_insert(:community) + + {:ok, post} = CMS.create_content(community, :post, mock_attrs(:post), user) + {:ok, job} = CMS.create_content(community, :job, mock_attrs(:job), user) + {:ok, video} = CMS.create_content(community, :video, mock_attrs(:video), user) + {:ok, repo} = CMS.create_content(community, :repo, mock_attrs(:repo), user) + + {:ok, ~m(user community post job video repo)a} + end + + describe "[cms post pin]" do + test "can pin a post", ~m(community post)a do + {:ok, pined_post} = CMS.pin_content(post, community, "posts") + + assert pined_post.id == post.id + end + + test "can undo pin to a post", ~m(community post)a do + {:ok, pined_post} = CMS.pin_content(post, community, "posts") + assert pined_post.id == post.id + + assert {:ok, unpined} = CMS.undo_pin_content(post, community, "posts") + assert unpined.id == post.id + end + end + + describe "[cms job pin]" do + test "can pin a job", ~m(community job)a do + {:ok, pined_job} = CMS.pin_content(job, community) + + assert pined_job.id == job.id + end + + test "can undo pin to a job", ~m(community job)a do + {:ok, pined_job} = CMS.pin_content(job, community) + assert pined_job.id == job.id + + assert {:ok, unpined} = CMS.undo_pin_content(job, community) + assert unpined.id == job.id + end + end + + describe "[cms video pin]" do + @tag :wip + test "can pin a video", ~m(community video)a do + {:ok, pined_video} = CMS.pin_content(video, community) + + assert pined_video.id == video.id + end + + @tag :wip + test "can undo pin to a video", ~m(community video)a do + {:ok, pined_video} = CMS.pin_content(video, community) + assert pined_video.id == video.id + + assert {:ok, unpined} = CMS.undo_pin_content(video, community) + assert unpined.id == video.id + end + end + + describe "[cms repo pin]" do + @tag :wip + test "can pin a repo", ~m(community repo)a do + {:ok, pined_repo} = CMS.pin_content(repo, community) + + assert pined_repo.id == repo.id + end + + @tag :wip + test "can undo pin to a repo", ~m(community repo)a do + {:ok, pined_repo} = CMS.pin_content(repo, community) + assert pined_repo.id == repo.id + + assert {:ok, unpined} = CMS.undo_pin_content(repo, community) + assert unpined.id == repo.id + end + end +end diff --git a/test/mastani_server_web/mutation/cms/job_flag_test.exs b/test/mastani_server_web/mutation/cms/job_flag_test.exs index 2eeb189bc..fa057eedf 100644 --- a/test/mastani_server_web/mutation/cms/job_flag_test.exs +++ b/test/mastani_server_web/mutation/cms/job_flag_test.exs @@ -84,6 +84,7 @@ defmodule MastaniServer.Test.Mutation.JobFlag do } } """ + @tag :wip test "auth user can pin job", ~m(community job)a do variables = %{id: job.id, communityId: community.id} @@ -95,6 +96,7 @@ defmodule MastaniServer.Test.Mutation.JobFlag do assert updated["id"] == to_string(job.id) end + @tag :wip test "unauth user pin job fails", ~m(user_conn guest_conn community job)a do variables = %{id: job.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) @@ -112,18 +114,21 @@ defmodule MastaniServer.Test.Mutation.JobFlag do } } """ + @tag :wip test "auth user can undo pin job", ~m(community job)a do variables = %{id: job.id, communityId: community.id} passport_rules = %{community.raw => %{"job.undo_pin" => true}} rule_conn = simu_conn(:user, cms: passport_rules) + CMS.pin_content(job, community) updated = rule_conn |> mutation_result(@query, variables, "undoPinJob") assert updated["id"] == to_string(job.id) - assert updated["pin"] == false + # assert updated["pin"] == false end + @tag :wip test "unauth user undo pin job fails", ~m(user_conn guest_conn community job)a do variables = %{id: job.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) diff --git a/test/mastani_server_web/mutation/cms/post_flag_test.exs b/test/mastani_server_web/mutation/cms/post_flag_test.exs index 2f33a12e0..f80247972 100644 --- a/test/mastani_server_web/mutation/cms/post_flag_test.exs +++ b/test/mastani_server_web/mutation/cms/post_flag_test.exs @@ -84,6 +84,7 @@ defmodule MastaniServer.Test.Mutation.PostFlag do } } """ + @tag :wip test "auth user can pin post", ~m(community post)a do variables = %{id: post.id, communityId: community.id} @@ -95,6 +96,7 @@ defmodule MastaniServer.Test.Mutation.PostFlag do assert updated["id"] == to_string(post.id) end + @tag :wip test "unauth user pin post fails", ~m(user_conn guest_conn community post)a do variables = %{id: post.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) @@ -112,18 +114,20 @@ defmodule MastaniServer.Test.Mutation.PostFlag do } } """ + @tag :wip test "auth user can undo pin post", ~m(community post)a do variables = %{id: post.id, communityId: community.id} passport_rules = %{community.raw => %{"post.undo_pin" => true}} rule_conn = simu_conn(:user, cms: passport_rules) + CMS.pin_content(post, community, "posts") updated = rule_conn |> mutation_result(@query, variables, "undoPinPost") assert updated["id"] == to_string(post.id) - assert updated["pin"] == false end + @tag :wip test "unauth user undo pin post fails", ~m(user_conn guest_conn community post)a do variables = %{id: post.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) diff --git a/test/mastani_server_web/mutation/cms/repo_flag_test.exs b/test/mastani_server_web/mutation/cms/repo_flag_test.exs index 9ae71804d..f4314d7a0 100644 --- a/test/mastani_server_web/mutation/cms/repo_flag_test.exs +++ b/test/mastani_server_web/mutation/cms/repo_flag_test.exs @@ -84,6 +84,7 @@ defmodule MastaniServer.Test.Mutation.RepoFlag do } } """ + @tag :wip test "auth user can pin repo", ~m(community repo)a do variables = %{id: repo.id, communityId: community.id} @@ -95,6 +96,7 @@ defmodule MastaniServer.Test.Mutation.RepoFlag do assert updated["id"] == to_string(repo.id) end + @tag :wip test "unauth user pin repo fails", ~m(user_conn guest_conn community repo)a do variables = %{id: repo.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) @@ -112,18 +114,20 @@ defmodule MastaniServer.Test.Mutation.RepoFlag do } } """ + @tag :wip test "auth user can undo pin repo", ~m(community repo)a do variables = %{id: repo.id, communityId: community.id} passport_rules = %{community.raw => %{"repo.undo_pin" => true}} rule_conn = simu_conn(:user, cms: passport_rules) + CMS.pin_content(repo, community) updated = rule_conn |> mutation_result(@query, variables, "undoPinRepo") assert updated["id"] == to_string(repo.id) - assert updated["pin"] == false end + @tag :wip test "unauth user undo pin repo fails", ~m(user_conn guest_conn community repo)a do variables = %{id: repo.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) diff --git a/test/mastani_server_web/mutation/cms/video_flag_test.exs b/test/mastani_server_web/mutation/cms/video_flag_test.exs index 678cd967f..a79c442c5 100644 --- a/test/mastani_server_web/mutation/cms/video_flag_test.exs +++ b/test/mastani_server_web/mutation/cms/video_flag_test.exs @@ -84,6 +84,7 @@ defmodule MastaniServer.Test.Mutation.VideoFlag do } } """ + @tag :wip test "auth user can pin video", ~m(community video)a do variables = %{id: video.id, communityId: community.id} @@ -95,6 +96,7 @@ defmodule MastaniServer.Test.Mutation.VideoFlag do assert updated["id"] == to_string(video.id) end + @tag :wip test "unauth user pin video fails", ~m(user_conn guest_conn community video)a do variables = %{id: video.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) @@ -108,22 +110,23 @@ defmodule MastaniServer.Test.Mutation.VideoFlag do mutation($id: ID!, $communityId: ID!){ undoPinVideo(id: $id, communityId: $communityId) { id - pin } } """ + @tag :wip test "auth user can undo pin video", ~m(community video)a do variables = %{id: video.id, communityId: community.id} passport_rules = %{community.raw => %{"video.undo_pin" => true}} rule_conn = simu_conn(:user, cms: passport_rules) + CMS.pin_content(video, community) updated = rule_conn |> mutation_result(@query, variables, "undoPinVideo") assert updated["id"] == to_string(video.id) - assert updated["pin"] == false end + @tag :wip test "unauth user undo pin video fails", ~m(user_conn guest_conn community video)a do variables = %{id: video.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) diff --git a/test/mastani_server_web/query/cms/jobs_flags_test.exs b/test/mastani_server_web/query/cms/jobs_flags_test.exs index a435a3872..636d31c42 100644 --- a/test/mastani_server_web/query/cms/jobs_flags_test.exs +++ b/test/mastani_server_web/query/cms/jobs_flags_test.exs @@ -51,6 +51,7 @@ defmodule MastaniServer.Test.Query.JobsFlags do } } """ + @tag :wip test "if have pined jobs, the pined jobs should at the top of entries", ~m(guest_conn community job_m)a do variables = %{filter: %{community: community.raw}} @@ -62,23 +63,25 @@ defmodule MastaniServer.Test.Query.JobsFlags do assert results["pageSize"] == @page_size assert results["totalCount"] == @total_count - CMS.set_community_flags(%Job{id: job_m.id}, community.id, %{pin: true}) + {:ok, _pined_post} = CMS.pin_content(job_m, community) results = guest_conn |> query_result(@query, variables, "pagedJobs") entries_first = results["entries"] |> List.first() - assert results["totalCount"] == @total_count + assert results["totalCount"] == @total_count + 1 assert entries_first["id"] == to_string(job_m.id) assert entries_first["pin"] == true end + @tag :wip test "pind jobs should not appear when page > 1", ~m(guest_conn community)a do variables = %{filter: %{page: 2, size: 20}} results = guest_conn |> query_result(@query, variables, "pagedJobs") assert results |> is_valid_pagination? random_id = results["entries"] |> Enum.shuffle() |> List.first() |> Map.get("id") - {:ok, _} = CMS.set_community_flags(%Job{id: random_id}, community.id, %{pin: true}) + {:ok, _pined_post} = CMS.pin_content(%Job{id: random_id}, community) + # {:ok, _} = CMS.set_community_flags(%Job{id: random_id}, community.id, %{pin: true}) results = guest_conn |> query_result(@query, variables, "pagedJobs") assert results["entries"] |> Enum.any?(&(&1["id"] !== random_id)) diff --git a/test/mastani_server_web/query/cms/posts_flags_test.exs b/test/mastani_server_web/query/cms/posts_flags_test.exs index b470b2df8..a54868584 100644 --- a/test/mastani_server_web/query/cms/posts_flags_test.exs +++ b/test/mastani_server_web/query/cms/posts_flags_test.exs @@ -51,6 +51,7 @@ defmodule MastaniServer.Test.Query.PostsFlags do } } """ + @tag :wip test "if have pined posts, the pined posts should at the top of entries", ~m(guest_conn community post_m)a do variables = %{filter: %{community: community.raw}} @@ -62,28 +63,33 @@ defmodule MastaniServer.Test.Query.PostsFlags do assert results["pageSize"] == @page_size assert results["totalCount"] == @total_count - CMS.set_community_flags(%Post{id: post_m.id}, community.id, %{pin: true}) + {:ok, _pined_post} = CMS.pin_content(post_m, community, "posts") results = guest_conn |> query_result(@query, variables, "pagedPosts") entries_first = results["entries"] |> List.first() - assert results["totalCount"] == @total_count + assert results["totalCount"] == @total_count + 1 assert entries_first["id"] == to_string(post_m.id) assert entries_first["pin"] == true end + @tag :wip test "pind posts should not appear when page > 1", ~m(guest_conn community)a do variables = %{filter: %{page: 2, size: 20}} results = guest_conn |> query_result(@query, variables, "pagedPosts") assert results |> is_valid_pagination? random_id = results["entries"] |> Enum.shuffle() |> List.first() |> Map.get("id") - {:ok, _} = CMS.set_community_flags(%Post{id: random_id}, community.id, %{pin: true}) + + {:ok, _pined_post} = CMS.pin_content(%Post{id: random_id}, community, "posts") + + # {:ok, _} = CMS.set_community_flags(%Post{id: random_id}, community.id, %{pin: true}) results = guest_conn |> query_result(@query, variables, "pagedPosts") assert results["entries"] |> Enum.any?(&(&1["id"] !== random_id)) end + @tag :wip test "if have trashed posts, the trashed posts should not appears in result", ~m(guest_conn community)a do variables = %{filter: %{community: community.raw}} diff --git a/test/mastani_server_web/query/cms/posts_topic_test.exs b/test/mastani_server_web/query/cms/posts_topic_test.exs index 09597adb7..9db381ef3 100644 --- a/test/mastani_server_web/query/cms/posts_topic_test.exs +++ b/test/mastani_server_web/query/cms/posts_topic_test.exs @@ -53,6 +53,7 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ + @tag :wip2 test "create post with valid args and topic ", ~m(guest_conn)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -61,7 +62,7 @@ defmodule MastaniServer.Test.Query.PostsTopic do post_attr = mock_attrs(:post) variables = post_attr |> Map.merge(%{communityId: community.id, topic: "city"}) - _created = user_conn |> mutation_result(@create_post_query, variables, "createPost") + created = user_conn |> mutation_result(@create_post_query, variables, "createPost") variables = %{filter: %{page: 1, size: 10, topic: "city"}} results = guest_conn |> query_result(@query, variables, "pagedPosts") @@ -80,6 +81,7 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ + @tag :wip2 test "topic filter on posts should work", ~m(guest_conn)a do variables = %{filter: %{page: 1, size: 10}} results = guest_conn |> query_result(@query, variables, "pagedPosts") diff --git a/test/mastani_server_web/query/cms/repos_flags_test.exs b/test/mastani_server_web/query/cms/repos_flags_test.exs index 03e31f643..80005f96a 100644 --- a/test/mastani_server_web/query/cms/repos_flags_test.exs +++ b/test/mastani_server_web/query/cms/repos_flags_test.exs @@ -50,6 +50,7 @@ defmodule MastaniServer.Test.Query.ReposFlags do } } """ + @tag :wip test "if have pined repos, the pined repos should at the top of entries", ~m(guest_conn community repo_m)a do variables = %{filter: %{community: community.raw}} @@ -61,28 +62,31 @@ defmodule MastaniServer.Test.Query.ReposFlags do assert results["pageSize"] == @page_size assert results["totalCount"] == @total_count - CMS.set_community_flags(%Repo{id: repo_m.id}, community.id, %{pin: true}) + {:ok, _pined_post} = CMS.pin_content(repo_m, community) results = guest_conn |> query_result(@query, variables, "pagedRepos") entries_first = results["entries"] |> List.first() - assert results["totalCount"] == @total_count + assert results["totalCount"] == @total_count + 1 assert entries_first["id"] == to_string(repo_m.id) assert entries_first["pin"] == true end + @tag :wip test "pind repos should not appear when page > 1", ~m(guest_conn community)a do variables = %{filter: %{page: 2, size: 20}} results = guest_conn |> query_result(@query, variables, "pagedRepos") assert results |> is_valid_pagination? random_id = results["entries"] |> Enum.shuffle() |> List.first() |> Map.get("id") - {:ok, _} = CMS.set_community_flags(%Repo{id: random_id}, community.id, %{pin: true}) + + {:ok, _pined_post} = CMS.pin_content(%Repo{id: random_id}, community) results = guest_conn |> query_result(@query, variables, "pagedRepos") assert results["entries"] |> Enum.any?(&(&1["id"] !== random_id)) end + @tag :wip test "if have trashed repos, the trashed repos should not appears in result", ~m(guest_conn community)a do variables = %{filter: %{community: community.raw}} diff --git a/test/mastani_server_web/query/cms/videos_flags_test.exs b/test/mastani_server_web/query/cms/videos_flags_test.exs index 8436cd032..d6927140b 100644 --- a/test/mastani_server_web/query/cms/videos_flags_test.exs +++ b/test/mastani_server_web/query/cms/videos_flags_test.exs @@ -50,6 +50,7 @@ defmodule MastaniServer.Test.Query.VideosFlags do } } """ + @tag :wip test "if have pined videos, the pined videos should at the top of entries", ~m(guest_conn community video_m)a do variables = %{filter: %{community: community.raw}} @@ -61,16 +62,17 @@ defmodule MastaniServer.Test.Query.VideosFlags do assert results["pageSize"] == @page_size assert results["totalCount"] == @total_count - CMS.set_community_flags(%Video{id: video_m.id}, community.id, %{pin: true}) + {:ok, _pined_post} = CMS.pin_content(video_m, community) results = guest_conn |> query_result(@query, variables, "pagedVideos") entries_first = results["entries"] |> List.first() - assert results["totalCount"] == @total_count + assert results["totalCount"] == @total_count + 1 assert entries_first["id"] == to_string(video_m.id) assert entries_first["pin"] == true end + @tag :wip test "pind videos should not appear when page > 1", ~m(guest_conn community)a do variables = %{filter: %{page: 2, size: 20}} results = guest_conn |> query_result(@query, variables, "pagedVideos") @@ -83,6 +85,7 @@ defmodule MastaniServer.Test.Query.VideosFlags do assert results["entries"] |> Enum.any?(&(&1["id"] !== random_id)) end + @tag :wip test "if have trashed videos, the trashed videos should not appears in result", ~m(guest_conn community)a do variables = %{filter: %{community: community.raw}} From 83926cc127cb8fd0796ef1576f9ba64040dc98a0 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 3 Nov 2018 01:44:18 +0800 Subject: [PATCH 124/129] refactor: reduce boil-code --- .../cms/delegates/article_curd.ex | 236 ++++++++---------- .../cms/delegates/article_operation.ex | 29 +-- 2 files changed, 107 insertions(+), 158 deletions(-) diff --git a/lib/mastani_server/cms/delegates/article_curd.ex b/lib/mastani_server/cms/delegates/article_curd.ex index 4b499d20a..60d105fb5 100644 --- a/lib/mastani_server/cms/delegates/article_curd.ex +++ b/lib/mastani_server/cms/delegates/article_curd.ex @@ -46,139 +46,6 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do |> add_pin_contents_ifneed(queryable, filter) end - defp flag_query(queryable, filter, flag \\ %{}) do - flag = %{trash: false} |> Map.merge(flag) - - # NOTE: this case judge is used for test case - case filter |> Map.has_key?(:community) do - true -> - queryable - |> join(:inner, [q], f in assoc(q, :community_flags)) - |> where([q, f], f.trash == ^flag.trash) - |> join(:inner, [q, f], c in assoc(f, :community)) - |> where([q, f, c], c.raw == ^filter.community) - - false -> - queryable - end - end - - # only first page need pin contents - # TODO: use seperate pined table, which is much more smaller - defp add_pin_contents_ifneed(contents, CMS.Post, %{community: community} = filter) do - with {:ok, normal_contents} <- contents, - true <- Map.has_key?(filter, :community), - true <- 1 == Map.get(normal_contents, :page_number) do - {:ok, pined_content} = - CMS.PinedPost - |> join(:inner, [p], c in assoc(p, :community)) - |> join(:inner, [p], t in assoc(p, :topic)) - |> join(:inner, [p], content in assoc(p, :post)) - |> where( - [p, c, t, content], - c.raw == ^filter.community and t.raw == ^Map.get(filter, :topic, "posts") - ) - |> select([p, c, t, content], content) - # 10 pined contents per community/thread, at most - |> ORM.paginater(%{page: 1, size: 10}) - |> done() - - concat_contents(pined_content, normal_contents) - else - _error -> - contents - end - end - - defp add_pin_contents_ifneed(contents, CMS.Job, %{community: community} = filter) do - with {:ok, normal_contents} <- contents, - true <- Map.has_key?(filter, :community), - true <- 1 == Map.get(normal_contents, :page_number) do - {:ok, pined_content} = - CMS.PinedJob - |> join(:inner, [p], c in assoc(p, :community)) - |> join(:inner, [p], content in assoc(p, :job)) - |> where([p, c, content], c.raw == ^filter.community) - |> select([p, c, content], content) - # 10 pined contents per community/thread, at most - |> ORM.paginater(%{page: 1, size: 10}) - |> done() - - concat_contents(pined_content, normal_contents) - else - _error -> - contents - end - end - - defp add_pin_contents_ifneed(contents, CMS.Video, %{community: community} = filter) do - with {:ok, normal_contents} <- contents, - true <- Map.has_key?(filter, :community), - true <- 1 == Map.get(normal_contents, :page_number) do - {:ok, pined_content} = - CMS.PinedVideo - |> join(:inner, [p], c in assoc(p, :community)) - |> join(:inner, [p], content in assoc(p, :video)) - |> where([p, c, content], c.raw == ^filter.community) - |> select([p, c, content], content) - # 10 pined contents per community/thread, at most - |> ORM.paginater(%{page: 1, size: 10}) - |> done() - - concat_contents(pined_content, normal_contents) - else - _error -> - contents - end - end - - defp add_pin_contents_ifneed(contents, CMS.Repo, %{community: community} = filter) do - with {:ok, normal_contents} <- contents, - true <- Map.has_key?(filter, :community), - true <- 1 == Map.get(normal_contents, :page_number) do - {:ok, pined_content} = - CMS.PinedRepo - |> join(:inner, [p], c in assoc(p, :community)) - |> join(:inner, [p], content in assoc(p, :repo)) - |> where([p, c, content], c.raw == ^filter.community) - |> select([p, c, content], content) - # 10 pined contents per community/thread, at most - |> ORM.paginater(%{page: 1, size: 10}) - |> done() - - concat_contents(pined_content, normal_contents) - else - _error -> - contents - end - end - - defp add_pin_contents_ifneed(contents, _querable, _filter), do: contents - - defp concat_contents(pined_content, normal_contents) do - case pined_content |> Map.get(:total_count) do - 0 -> - {:ok, normal_contents} - - _ -> - # NOTE: this is tricy, should use dataloader refactor - pind_entries = - pined_content - |> Map.get(:entries) - |> Enum.map(&struct(&1, %{pin: true})) - - normal_entries = normal_contents |> Map.get(:entries) - - normal_count = normal_contents |> Map.get(:total_count) - pind_count = pined_content |> Map.get(:total_count) - - normal_contents - |> Map.put(:entries, pind_entries ++ normal_entries) - |> Map.put(:total_count, pind_count + normal_count) - |> done - end - end - @doc """ Creates a content(post/job ...), and set community. @@ -328,4 +195,107 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do defp handle_existing_author({:error, changeset}) do ORM.find_by(Author, user_id: changeset.data.user_id) end + + defp flag_query(queryable, filter, flag \\ %{}) do + flag = %{trash: false} |> Map.merge(flag) + + # NOTE: this case judge is used for test case + case filter |> Map.has_key?(:community) do + true -> + queryable + |> join(:inner, [q], f in assoc(q, :community_flags)) + |> where([q, f], f.trash == ^flag.trash) + |> join(:inner, [q, f], c in assoc(f, :community)) + |> where([q, f, c], c.raw == ^filter.community) + + false -> + queryable + end + end + + # only first page need pin contents + # TODO: use seperate pined table, which is much more smaller + defp add_pin_contents_ifneed(contents, CMS.Post, %{community: community} = filter) do + with {:ok, normal_contents} <- contents, + true <- Map.has_key?(filter, :community), + true <- 1 == Map.get(normal_contents, :page_number) do + {:ok, pined_content} = + CMS.PinedPost + |> join(:inner, [p], c in assoc(p, :community)) + |> join(:inner, [p], t in assoc(p, :topic)) + |> join(:inner, [p], content in assoc(p, :post)) + |> where( + [p, c, t, content], + c.raw == ^filter.community and t.raw == ^Map.get(filter, :topic, "posts") + ) + |> select([p, c, t, content], content) + # 10 pined contents per community/thread, at most + |> ORM.paginater(%{page: 1, size: 10}) + |> done() + + concat_contents(pined_content, normal_contents) + else + _error -> + contents + end + end + + defp add_pin_contents_ifneed(contents, CMS.Job, %{community: community} = filter) do + merge_pin_contents(contents, :job, CMS.PinedJob, filter) + end + + defp add_pin_contents_ifneed(contents, CMS.Video, %{community: community} = filter) do + merge_pin_contents(contents, :video, CMS.PinedVideo, filter) + end + + defp add_pin_contents_ifneed(contents, CMS.Repo, %{community: community} = filter) do + merge_pin_contents(contents, :repo, CMS.PinedRepo, filter) + end + + defp merge_pin_contents(contents, thread, pin_schema, %{community: community} = filter) do + with {:ok, normal_contents} <- contents, + true <- Map.has_key?(filter, :community), + true <- 1 == Map.get(normal_contents, :page_number) do + {:ok, pined_content} = + pin_schema + |> join(:inner, [p], c in assoc(p, :community)) + |> join(:inner, [p], content in assoc(p, ^thread)) + |> where([p, c, content], c.raw == ^filter.community) + |> select([p, c, content], content) + # 10 pined contents per community/thread, at most + |> ORM.paginater(%{page: 1, size: 10}) + |> done() + + concat_contents(pined_content, normal_contents) + else + _error -> + contents + end + end + + defp add_pin_contents_ifneed(contents, _querable, _filter), do: contents + + defp concat_contents(pined_content, normal_contents) do + case pined_content |> Map.get(:total_count) do + 0 -> + {:ok, normal_contents} + + _ -> + # NOTE: this is tricy, should use dataloader refactor + pind_entries = + pined_content + |> Map.get(:entries) + |> Enum.map(&struct(&1, %{pin: true})) + + normal_entries = normal_contents |> Map.get(:entries) + + normal_count = normal_contents |> Map.get(:total_count) + pind_count = pined_content |> Map.get(:total_count) + + normal_contents + |> Map.put(:entries, pind_entries ++ normal_entries) + |> Map.put(:total_count, pind_count + normal_count) + |> done + end + end end diff --git a/lib/mastani_server/cms/delegates/article_operation.ex b/lib/mastani_server/cms/delegates/article_operation.ex index 79e3c023d..25c9208d0 100644 --- a/lib/mastani_server/cms/delegates/article_operation.ex +++ b/lib/mastani_server/cms/delegates/article_operation.ex @@ -43,13 +43,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleOperation do def undo_pin_content(%Post{id: post_id}, %Community{id: community_id}, topic) do with {:ok, %{id: topic_id}} <- ORM.find_by(Topic, %{raw: topic}), - {:ok, pined} <- - ORM.find_by( - PinedPost, - post_id: post_id, - community_id: community_id, - topic_id: topic_id - ), + {:ok, pined} <- ORM.find_by(PinedPost, ~m(post_id community_id topic_id)a), {:ok, deleted} <- ORM.delete(pined) do Post |> ORM.find(deleted.post_id) end @@ -64,12 +58,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleOperation do end def undo_pin_content(%Job{id: job_id}, %Community{id: community_id}) do - with {:ok, pined} <- - ORM.find_by( - PinedJob, - job_id: job_id, - community_id: community_id - ), + with {:ok, pined} <- ORM.find_by(PinedJob, ~m(job_id community_id)a), {:ok, deleted} <- ORM.delete(pined) do Job |> ORM.find(deleted.job_id) end @@ -84,12 +73,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleOperation do end def undo_pin_content(%Video{id: video_id}, %Community{id: community_id}) do - with {:ok, pined} <- - ORM.find_by( - PinedVideo, - video_id: video_id, - community_id: community_id - ), + with {:ok, pined} <- ORM.find_by(PinedVideo, ~m(video_id community_id)a), {:ok, deleted} <- ORM.delete(pined) do Video |> ORM.find(deleted.video_id) end @@ -104,12 +88,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleOperation do end def undo_pin_content(%CMSRepo{id: repo_id}, %Community{id: community_id}) do - with {:ok, pined} <- - ORM.find_by( - PinedRepo, - repo_id: repo_id, - community_id: community_id - ), + with {:ok, pined} <- ORM.find_by(PinedRepo, ~m(repo_id community_id)a), {:ok, deleted} <- ORM.delete(pined) do CMSRepo |> ORM.find(deleted.repo_id) end From c02d827c77f169361a09903b7379ed86b31a2b70 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 3 Nov 2018 01:55:26 +0800 Subject: [PATCH 125/129] refactor(pined contents): remove old pin state from communityFlags --- .../cms/delegates/article_curd.ex | 1 - .../cms/delegates/article_operation.ex | 2 +- lib/mastani_server/cms/job_community_flag.ex | 3 +-- lib/mastani_server/cms/post_community_flag.ex | 4 +--- lib/mastani_server/cms/repo_community_flag.ex | 3 +-- .../cms/video_community_flag.ex | 3 +-- ...02174534_remove_pin_in_community_flags.exs | 22 +++++++++++++++++++ test/mastani_server/cms/content_flag_test.exs | 2 +- .../query/cms/posts_topic_test.exs | 6 ++--- 9 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 priv/repo/migrations/20181102174534_remove_pin_in_community_flags.exs diff --git a/lib/mastani_server/cms/delegates/article_curd.ex b/lib/mastani_server/cms/delegates/article_curd.ex index 60d105fb5..eef73cc85 100644 --- a/lib/mastani_server/cms/delegates/article_curd.ex +++ b/lib/mastani_server/cms/delegates/article_curd.ex @@ -92,7 +92,6 @@ defmodule MastaniServer.CMS.Delegate.ArticleCURD do case action |> Map.has_key?(:flag) do true -> ArticleOperation.set_community_flags(content, community.id, %{ - pin: false, trash: false }) diff --git a/lib/mastani_server/cms/delegates/article_operation.ex b/lib/mastani_server/cms/delegates/article_operation.ex index 25c9208d0..34fe572ae 100644 --- a/lib/mastani_server/cms/delegates/article_operation.ex +++ b/lib/mastani_server/cms/delegates/article_operation.ex @@ -113,7 +113,7 @@ defmodule MastaniServer.CMS.Delegate.ArticleOperation do with {:ok, content} <- ORM.find(content.__struct__, content.id), {:ok, community} <- ORM.find(Community, community_id), {:ok, record} <- insert_flag_record(content, community.id, attrs) do - {:ok, struct(content, %{pin: record.pin, trash: record.trash})} + {:ok, struct(content, %{trash: record.trash})} end end diff --git a/lib/mastani_server/cms/job_community_flag.ex b/lib/mastani_server/cms/job_community_flag.ex index 4919c4a85..b7cd9db97 100644 --- a/lib/mastani_server/cms/job_community_flag.ex +++ b/lib/mastani_server/cms/job_community_flag.ex @@ -7,7 +7,7 @@ defmodule MastaniServer.CMS.JobCommunityFlag do alias MastaniServer.CMS.{Community, Job} @required_fields ~w(job_id community_id)a - @optional_fields ~w(pin trash)a + @optional_fields ~w(trash)a @type t :: %JobCommunityFlag{} @@ -15,7 +15,6 @@ defmodule MastaniServer.CMS.JobCommunityFlag do belongs_to(:job, Job, foreign_key: :job_id) belongs_to(:community, Community, foreign_key: :community_id) - field(:pin, :boolean) field(:trash, :boolean) timestamps(type: :utc_datetime) diff --git a/lib/mastani_server/cms/post_community_flag.ex b/lib/mastani_server/cms/post_community_flag.ex index a8aadd99c..47af39b72 100644 --- a/lib/mastani_server/cms/post_community_flag.ex +++ b/lib/mastani_server/cms/post_community_flag.ex @@ -7,7 +7,7 @@ defmodule MastaniServer.CMS.PostCommunityFlag do alias MastaniServer.CMS.{Community, Post} @required_fields ~w(post_id community_id)a - @optional_fields ~w(pin trash refined)a + @optional_fields ~w(trash)a @type t :: %PostCommunityFlag{} @@ -15,8 +15,6 @@ defmodule MastaniServer.CMS.PostCommunityFlag do belongs_to(:post, Post, foreign_key: :post_id) belongs_to(:community, Community, foreign_key: :community_id) - field(:pin, :boolean) - field(:refined, :boolean) field(:trash, :boolean) timestamps(type: :utc_datetime) diff --git a/lib/mastani_server/cms/repo_community_flag.ex b/lib/mastani_server/cms/repo_community_flag.ex index d447ad9a8..4c5cbe7aa 100644 --- a/lib/mastani_server/cms/repo_community_flag.ex +++ b/lib/mastani_server/cms/repo_community_flag.ex @@ -7,7 +7,7 @@ defmodule MastaniServer.CMS.RepoCommunityFlag do alias MastaniServer.CMS.{Community, Repo} @required_fields ~w(repo_id community_id)a - @optional_fields ~w(pin trash)a + @optional_fields ~w(trash)a @type t :: %RepoCommunityFlag{} @@ -15,7 +15,6 @@ defmodule MastaniServer.CMS.RepoCommunityFlag do belongs_to(:repo, Repo, foreign_key: :repo_id) belongs_to(:community, Community, foreign_key: :community_id) - field(:pin, :boolean) field(:trash, :boolean) timestamps(type: :utc_datetime) diff --git a/lib/mastani_server/cms/video_community_flag.ex b/lib/mastani_server/cms/video_community_flag.ex index 3ab06dce5..94382031d 100644 --- a/lib/mastani_server/cms/video_community_flag.ex +++ b/lib/mastani_server/cms/video_community_flag.ex @@ -7,7 +7,7 @@ defmodule MastaniServer.CMS.VideoCommunityFlag do alias MastaniServer.CMS.{Community, Video} @required_fields ~w(video_id community_id)a - @optional_fields ~w(pin trash)a + @optional_fields ~w(trash)a @type t :: %VideoCommunityFlag{} @@ -15,7 +15,6 @@ defmodule MastaniServer.CMS.VideoCommunityFlag do belongs_to(:video, Video, foreign_key: :video_id) belongs_to(:community, Community, foreign_key: :community_id) - field(:pin, :boolean) field(:trash, :boolean) timestamps(type: :utc_datetime) diff --git a/priv/repo/migrations/20181102174534_remove_pin_in_community_flags.exs b/priv/repo/migrations/20181102174534_remove_pin_in_community_flags.exs new file mode 100644 index 000000000..5a127b80e --- /dev/null +++ b/priv/repo/migrations/20181102174534_remove_pin_in_community_flags.exs @@ -0,0 +1,22 @@ +defmodule MastaniServer.Repo.Migrations.RemovePinInCommunityFlags do + use Ecto.Migration + + def change do + alter table(:posts_communities_flags) do + remove(:pin) + remove(:refined) + end + + alter table(:jobs_communities_flags) do + remove(:pin) + end + + alter table(:videos_communities_flags) do + remove(:pin) + end + + alter table(:repos_communities_flags) do + remove(:pin) + end + end +end diff --git a/test/mastani_server/cms/content_flag_test.exs b/test/mastani_server/cms/content_flag_test.exs index d5292a099..7c67f5262 100644 --- a/test/mastani_server/cms/content_flag_test.exs +++ b/test/mastani_server/cms/content_flag_test.exs @@ -87,7 +87,7 @@ defmodule MastaniServer.Test.ContentFlags do end describe "[cms repo flag]" do - @tag :wip + @tag :wip2 test "user can set trash flag on repo", ~m(community repo)a do community_id = community.id repo_id = repo.id diff --git a/test/mastani_server_web/query/cms/posts_topic_test.exs b/test/mastani_server_web/query/cms/posts_topic_test.exs index 9db381ef3..5e0d88923 100644 --- a/test/mastani_server_web/query/cms/posts_topic_test.exs +++ b/test/mastani_server_web/query/cms/posts_topic_test.exs @@ -53,7 +53,7 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ - @tag :wip2 + @tag :wip test "create post with valid args and topic ", ~m(guest_conn)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -62,7 +62,7 @@ defmodule MastaniServer.Test.Query.PostsTopic do post_attr = mock_attrs(:post) variables = post_attr |> Map.merge(%{communityId: community.id, topic: "city"}) - created = user_conn |> mutation_result(@create_post_query, variables, "createPost") + _created = user_conn |> mutation_result(@create_post_query, variables, "createPost") variables = %{filter: %{page: 1, size: 10, topic: "city"}} results = guest_conn |> query_result(@query, variables, "pagedPosts") @@ -81,7 +81,7 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ - @tag :wip2 + @tag :wip test "topic filter on posts should work", ~m(guest_conn)a do variables = %{filter: %{page: 1, size: 10}} results = guest_conn |> query_result(@query, variables, "pagedPosts") From a820979ae2a360c4376777748cc08c0716645246 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 3 Nov 2018 01:58:21 +0800 Subject: [PATCH 126/129] chore(clean up): wip tags --- test/mastani_server/cms/content_flag_test.exs | 4 ---- test/mastani_server/cms/content_pin_test.exs | 4 ---- test/mastani_server_web/mutation/cms/job_flag_test.exs | 4 ---- test/mastani_server_web/mutation/cms/post_flag_test.exs | 4 ---- test/mastani_server_web/mutation/cms/repo_flag_test.exs | 4 ---- test/mastani_server_web/mutation/cms/video_flag_test.exs | 4 ---- test/mastani_server_web/query/cms/jobs_flags_test.exs | 2 -- test/mastani_server_web/query/cms/posts_flags_test.exs | 3 --- test/mastani_server_web/query/cms/posts_topic_test.exs | 2 -- test/mastani_server_web/query/cms/repos_flags_test.exs | 3 --- test/mastani_server_web/query/cms/videos_flags_test.exs | 3 --- 11 files changed, 37 deletions(-) diff --git a/test/mastani_server/cms/content_flag_test.exs b/test/mastani_server/cms/content_flag_test.exs index 7c67f5262..aeec53df5 100644 --- a/test/mastani_server/cms/content_flag_test.exs +++ b/test/mastani_server/cms/content_flag_test.exs @@ -29,7 +29,6 @@ defmodule MastaniServer.Test.ContentFlags do end describe "[cms post flag]" do - @tag :wip test "user can set trash flag on post based on community", ~m(community post)a do community_id = community.id post_id = post.id @@ -49,7 +48,6 @@ defmodule MastaniServer.Test.ContentFlags do end describe "[cms job flag]" do - @tag :wip test "user can set trash flag on job", ~m(community job)a do community_id = community.id job_id = job.id @@ -68,7 +66,6 @@ defmodule MastaniServer.Test.ContentFlags do end describe "[cms video flag]" do - @tag :wip test "user can set trash flag on a video", ~m(community video)a do community_id = community.id video_id = video.id @@ -87,7 +84,6 @@ defmodule MastaniServer.Test.ContentFlags do end describe "[cms repo flag]" do - @tag :wip2 test "user can set trash flag on repo", ~m(community repo)a do community_id = community.id repo_id = repo.id diff --git a/test/mastani_server/cms/content_pin_test.exs b/test/mastani_server/cms/content_pin_test.exs index 164a65ba2..7a0120d6b 100644 --- a/test/mastani_server/cms/content_pin_test.exs +++ b/test/mastani_server/cms/content_pin_test.exs @@ -56,14 +56,12 @@ defmodule MastaniServer.Test.ContentsPin do end describe "[cms video pin]" do - @tag :wip test "can pin a video", ~m(community video)a do {:ok, pined_video} = CMS.pin_content(video, community) assert pined_video.id == video.id end - @tag :wip test "can undo pin to a video", ~m(community video)a do {:ok, pined_video} = CMS.pin_content(video, community) assert pined_video.id == video.id @@ -74,14 +72,12 @@ defmodule MastaniServer.Test.ContentsPin do end describe "[cms repo pin]" do - @tag :wip test "can pin a repo", ~m(community repo)a do {:ok, pined_repo} = CMS.pin_content(repo, community) assert pined_repo.id == repo.id end - @tag :wip test "can undo pin to a repo", ~m(community repo)a do {:ok, pined_repo} = CMS.pin_content(repo, community) assert pined_repo.id == repo.id diff --git a/test/mastani_server_web/mutation/cms/job_flag_test.exs b/test/mastani_server_web/mutation/cms/job_flag_test.exs index fa057eedf..83729bdc5 100644 --- a/test/mastani_server_web/mutation/cms/job_flag_test.exs +++ b/test/mastani_server_web/mutation/cms/job_flag_test.exs @@ -84,7 +84,6 @@ defmodule MastaniServer.Test.Mutation.JobFlag do } } """ - @tag :wip test "auth user can pin job", ~m(community job)a do variables = %{id: job.id, communityId: community.id} @@ -96,7 +95,6 @@ defmodule MastaniServer.Test.Mutation.JobFlag do assert updated["id"] == to_string(job.id) end - @tag :wip test "unauth user pin job fails", ~m(user_conn guest_conn community job)a do variables = %{id: job.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) @@ -114,7 +112,6 @@ defmodule MastaniServer.Test.Mutation.JobFlag do } } """ - @tag :wip test "auth user can undo pin job", ~m(community job)a do variables = %{id: job.id, communityId: community.id} @@ -128,7 +125,6 @@ defmodule MastaniServer.Test.Mutation.JobFlag do # assert updated["pin"] == false end - @tag :wip test "unauth user undo pin job fails", ~m(user_conn guest_conn community job)a do variables = %{id: job.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) diff --git a/test/mastani_server_web/mutation/cms/post_flag_test.exs b/test/mastani_server_web/mutation/cms/post_flag_test.exs index f80247972..f9d1b8f47 100644 --- a/test/mastani_server_web/mutation/cms/post_flag_test.exs +++ b/test/mastani_server_web/mutation/cms/post_flag_test.exs @@ -84,7 +84,6 @@ defmodule MastaniServer.Test.Mutation.PostFlag do } } """ - @tag :wip test "auth user can pin post", ~m(community post)a do variables = %{id: post.id, communityId: community.id} @@ -96,7 +95,6 @@ defmodule MastaniServer.Test.Mutation.PostFlag do assert updated["id"] == to_string(post.id) end - @tag :wip test "unauth user pin post fails", ~m(user_conn guest_conn community post)a do variables = %{id: post.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) @@ -114,7 +112,6 @@ defmodule MastaniServer.Test.Mutation.PostFlag do } } """ - @tag :wip test "auth user can undo pin post", ~m(community post)a do variables = %{id: post.id, communityId: community.id} @@ -127,7 +124,6 @@ defmodule MastaniServer.Test.Mutation.PostFlag do assert updated["id"] == to_string(post.id) end - @tag :wip test "unauth user undo pin post fails", ~m(user_conn guest_conn community post)a do variables = %{id: post.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) diff --git a/test/mastani_server_web/mutation/cms/repo_flag_test.exs b/test/mastani_server_web/mutation/cms/repo_flag_test.exs index f4314d7a0..d2f813bc3 100644 --- a/test/mastani_server_web/mutation/cms/repo_flag_test.exs +++ b/test/mastani_server_web/mutation/cms/repo_flag_test.exs @@ -84,7 +84,6 @@ defmodule MastaniServer.Test.Mutation.RepoFlag do } } """ - @tag :wip test "auth user can pin repo", ~m(community repo)a do variables = %{id: repo.id, communityId: community.id} @@ -96,7 +95,6 @@ defmodule MastaniServer.Test.Mutation.RepoFlag do assert updated["id"] == to_string(repo.id) end - @tag :wip test "unauth user pin repo fails", ~m(user_conn guest_conn community repo)a do variables = %{id: repo.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) @@ -114,7 +112,6 @@ defmodule MastaniServer.Test.Mutation.RepoFlag do } } """ - @tag :wip test "auth user can undo pin repo", ~m(community repo)a do variables = %{id: repo.id, communityId: community.id} @@ -127,7 +124,6 @@ defmodule MastaniServer.Test.Mutation.RepoFlag do assert updated["id"] == to_string(repo.id) end - @tag :wip test "unauth user undo pin repo fails", ~m(user_conn guest_conn community repo)a do variables = %{id: repo.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) diff --git a/test/mastani_server_web/mutation/cms/video_flag_test.exs b/test/mastani_server_web/mutation/cms/video_flag_test.exs index a79c442c5..764f4fedd 100644 --- a/test/mastani_server_web/mutation/cms/video_flag_test.exs +++ b/test/mastani_server_web/mutation/cms/video_flag_test.exs @@ -84,7 +84,6 @@ defmodule MastaniServer.Test.Mutation.VideoFlag do } } """ - @tag :wip test "auth user can pin video", ~m(community video)a do variables = %{id: video.id, communityId: community.id} @@ -96,7 +95,6 @@ defmodule MastaniServer.Test.Mutation.VideoFlag do assert updated["id"] == to_string(video.id) end - @tag :wip test "unauth user pin video fails", ~m(user_conn guest_conn community video)a do variables = %{id: video.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) @@ -113,7 +111,6 @@ defmodule MastaniServer.Test.Mutation.VideoFlag do } } """ - @tag :wip test "auth user can undo pin video", ~m(community video)a do variables = %{id: video.id, communityId: community.id} @@ -126,7 +123,6 @@ defmodule MastaniServer.Test.Mutation.VideoFlag do assert updated["id"] == to_string(video.id) end - @tag :wip test "unauth user undo pin video fails", ~m(user_conn guest_conn community video)a do variables = %{id: video.id, communityId: community.id} rule_conn = simu_conn(:user, cms: %{"what.ever" => true}) diff --git a/test/mastani_server_web/query/cms/jobs_flags_test.exs b/test/mastani_server_web/query/cms/jobs_flags_test.exs index 636d31c42..e8be98223 100644 --- a/test/mastani_server_web/query/cms/jobs_flags_test.exs +++ b/test/mastani_server_web/query/cms/jobs_flags_test.exs @@ -51,7 +51,6 @@ defmodule MastaniServer.Test.Query.JobsFlags do } } """ - @tag :wip test "if have pined jobs, the pined jobs should at the top of entries", ~m(guest_conn community job_m)a do variables = %{filter: %{community: community.raw}} @@ -73,7 +72,6 @@ defmodule MastaniServer.Test.Query.JobsFlags do assert entries_first["pin"] == true end - @tag :wip test "pind jobs should not appear when page > 1", ~m(guest_conn community)a do variables = %{filter: %{page: 2, size: 20}} results = guest_conn |> query_result(@query, variables, "pagedJobs") diff --git a/test/mastani_server_web/query/cms/posts_flags_test.exs b/test/mastani_server_web/query/cms/posts_flags_test.exs index a54868584..dd0828504 100644 --- a/test/mastani_server_web/query/cms/posts_flags_test.exs +++ b/test/mastani_server_web/query/cms/posts_flags_test.exs @@ -51,7 +51,6 @@ defmodule MastaniServer.Test.Query.PostsFlags do } } """ - @tag :wip test "if have pined posts, the pined posts should at the top of entries", ~m(guest_conn community post_m)a do variables = %{filter: %{community: community.raw}} @@ -73,7 +72,6 @@ defmodule MastaniServer.Test.Query.PostsFlags do assert entries_first["pin"] == true end - @tag :wip test "pind posts should not appear when page > 1", ~m(guest_conn community)a do variables = %{filter: %{page: 2, size: 20}} results = guest_conn |> query_result(@query, variables, "pagedPosts") @@ -89,7 +87,6 @@ defmodule MastaniServer.Test.Query.PostsFlags do assert results["entries"] |> Enum.any?(&(&1["id"] !== random_id)) end - @tag :wip test "if have trashed posts, the trashed posts should not appears in result", ~m(guest_conn community)a do variables = %{filter: %{community: community.raw}} diff --git a/test/mastani_server_web/query/cms/posts_topic_test.exs b/test/mastani_server_web/query/cms/posts_topic_test.exs index 5e0d88923..09597adb7 100644 --- a/test/mastani_server_web/query/cms/posts_topic_test.exs +++ b/test/mastani_server_web/query/cms/posts_topic_test.exs @@ -53,7 +53,6 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ - @tag :wip test "create post with valid args and topic ", ~m(guest_conn)a do {:ok, user} = db_insert(:user) user_conn = simu_conn(:user, user) @@ -81,7 +80,6 @@ defmodule MastaniServer.Test.Query.PostsTopic do } } """ - @tag :wip test "topic filter on posts should work", ~m(guest_conn)a do variables = %{filter: %{page: 1, size: 10}} results = guest_conn |> query_result(@query, variables, "pagedPosts") diff --git a/test/mastani_server_web/query/cms/repos_flags_test.exs b/test/mastani_server_web/query/cms/repos_flags_test.exs index 80005f96a..71608d93f 100644 --- a/test/mastani_server_web/query/cms/repos_flags_test.exs +++ b/test/mastani_server_web/query/cms/repos_flags_test.exs @@ -50,7 +50,6 @@ defmodule MastaniServer.Test.Query.ReposFlags do } } """ - @tag :wip test "if have pined repos, the pined repos should at the top of entries", ~m(guest_conn community repo_m)a do variables = %{filter: %{community: community.raw}} @@ -72,7 +71,6 @@ defmodule MastaniServer.Test.Query.ReposFlags do assert entries_first["pin"] == true end - @tag :wip test "pind repos should not appear when page > 1", ~m(guest_conn community)a do variables = %{filter: %{page: 2, size: 20}} results = guest_conn |> query_result(@query, variables, "pagedRepos") @@ -86,7 +84,6 @@ defmodule MastaniServer.Test.Query.ReposFlags do assert results["entries"] |> Enum.any?(&(&1["id"] !== random_id)) end - @tag :wip test "if have trashed repos, the trashed repos should not appears in result", ~m(guest_conn community)a do variables = %{filter: %{community: community.raw}} diff --git a/test/mastani_server_web/query/cms/videos_flags_test.exs b/test/mastani_server_web/query/cms/videos_flags_test.exs index d6927140b..93d3d488f 100644 --- a/test/mastani_server_web/query/cms/videos_flags_test.exs +++ b/test/mastani_server_web/query/cms/videos_flags_test.exs @@ -50,7 +50,6 @@ defmodule MastaniServer.Test.Query.VideosFlags do } } """ - @tag :wip test "if have pined videos, the pined videos should at the top of entries", ~m(guest_conn community video_m)a do variables = %{filter: %{community: community.raw}} @@ -72,7 +71,6 @@ defmodule MastaniServer.Test.Query.VideosFlags do assert entries_first["pin"] == true end - @tag :wip test "pind videos should not appear when page > 1", ~m(guest_conn community)a do variables = %{filter: %{page: 2, size: 20}} results = guest_conn |> query_result(@query, variables, "pagedVideos") @@ -85,7 +83,6 @@ defmodule MastaniServer.Test.Query.VideosFlags do assert results["entries"] |> Enum.any?(&(&1["id"] !== random_id)) end - @tag :wip test "if have trashed videos, the trashed videos should not appears in result", ~m(guest_conn community)a do variables = %{filter: %{community: community.raw}} From 32c6e2fa5923f6e6c0fc9bd6570e62bb6754adaa Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sat, 3 Nov 2018 23:27:47 +0800 Subject: [PATCH 127/129] refactor(customization): basic set/get workflow --- config/config.exs | 10 +++ lib/helper/utils.ex | 27 ++++-- lib/mastani_server/accounts/accounts.ex | 5 +- lib/mastani_server/accounts/customization.ex | 9 +- .../accounts/delegates/customization.ex | 44 ++++++++-- .../resolvers/accounts_resolver.ex | 16 ++++ .../schema/account/account_misc.ex | 22 +++++ .../schema/account/account_mutations.ex | 8 ++ .../schema/account/account_types.ex | 18 ++++ .../20181103023157_add_more_customization.exs | 13 +++ ...181103031609_alter_customization_theme.exs | 10 +++ .../accounts/customization_test.exs | 35 ++++---- .../mutation/accounts/customization_test.exs | 82 +++++++++++++++++++ .../query/accounts/customization_test.exs | 52 ++++++++++++ 14 files changed, 319 insertions(+), 32 deletions(-) create mode 100644 priv/repo/migrations/20181103023157_add_more_customization.exs create mode 100644 priv/repo/migrations/20181103031609_alter_customization_theme.exs create mode 100644 test/mastani_server_web/mutation/accounts/customization_test.exs create mode 100644 test/mastani_server_web/query/accounts/customization_test.exs diff --git a/config/config.exs b/config/config.exs index 3bf9a1674..84065f80f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -51,6 +51,16 @@ config :mastani_server, :general, user_achieve_favorite_weight: 2, user_achieve_follow_weight: 3 +config :mastani_server, :customization, + theme: "cyan", + community_chart: false, + brainwash_free: false, + banner_layout: "digest", + contents_layout: "digest", + content_divider: false, + mark_viewed: true, + display_density: "20" + config :mastani_server, MastaniServerWeb.Gettext, default_locale: "zh_CN", locales: ~w(en zh_CN) import_config "#{Mix.env()}.exs" diff --git a/lib/helper/utils.ex b/lib/helper/utils.ex index 3e602a763..d55682018 100644 --- a/lib/helper/utils.ex +++ b/lib/helper/utils.ex @@ -6,10 +6,20 @@ defmodule Helper.Utils do import Helper.ErrorHandler import Helper.ErrorCode - def get_config(section, key, app \\ :mastani_server) do + def get_config(section, key, app \\ :mastani_server) + + def get_config(section, :all, app) do + app + |> Application.get_env(section) + |> case do + nil -> "" + config -> config + end + end + + def get_config(section, key, app) do app |> Application.get_env(section) - # |> IO.inspect(label: "debug ci") |> case do nil -> "" config -> Keyword.get(config, key) @@ -121,10 +131,15 @@ defmodule Helper.Utils do def map_atom_value(attrs, :string) do results = Enum.map(attrs, fn {k, v} -> - if is_atom(v) do - {k, v |> to_string() |> String.downcase()} - else - {k, v} + cond do + v == true or v == false -> + {k, v} + + is_atom(v) -> + {k, v |> to_string() |> String.downcase()} + + true -> + {k, v} end end) diff --git a/lib/mastani_server/accounts/accounts.ex b/lib/mastani_server/accounts/accounts.ex index 0b9114f64..fc5b2cfa0 100644 --- a/lib/mastani_server/accounts/accounts.ex +++ b/lib/mastani_server/accounts/accounts.ex @@ -66,6 +66,7 @@ defmodule MastaniServer.Accounts do defdelegate has_purchased?(user, key), to: Billing # customization - defdelegate add_custom_setting(user, key, value), to: Customization - defdelegate add_custom_setting(user, key), to: Customization + defdelegate get_customization(user), to: Customization + defdelegate set_customization(user, key, value), to: Customization + defdelegate set_customization(user, options), to: Customization end diff --git a/lib/mastani_server/accounts/customization.ex b/lib/mastani_server/accounts/customization.ex index 828e0400f..f0afe6673 100644 --- a/lib/mastani_server/accounts/customization.ex +++ b/lib/mastani_server/accounts/customization.ex @@ -8,7 +8,7 @@ defmodule MastaniServer.Accounts.Customization do alias MastaniServer.Accounts.User @required_fields ~w(user_id)a - @optional_fields ~w(theme sidebar_layout community_chart brainwash_free)a + @optional_fields ~w(theme sidebar_layout community_chart brainwash_free banner_layout contents_layout content_divider mark_viewed display_density)a @type t :: %Customization{} schema "customizations" do @@ -19,6 +19,13 @@ defmodule MastaniServer.Accounts.Customization do field(:community_chart, :boolean) field(:brainwash_free, :boolean) + field(:banner_layout, :string) + field(:contents_layout, :string) + field(:content_divider, :boolean) + field(:mark_viewed, :boolean) + # TODO: change to number + field(:display_density, :string) + timestamps(type: :utc_datetime) end diff --git a/lib/mastani_server/accounts/delegates/customization.ex b/lib/mastani_server/accounts/delegates/customization.ex index 799b627be..f42785c82 100644 --- a/lib/mastani_server/accounts/delegates/customization.ex +++ b/lib/mastani_server/accounts/delegates/customization.ex @@ -1,23 +1,42 @@ defmodule MastaniServer.Accounts.Delegate.Customization do + @moduledoc """ + customization for user + """ import Ecto.Query, warn: false + import Helper.Utils, only: [get_config: 2, map_atom_value: 2] + alias Helper.ORM alias MastaniServer.Accounts alias MastaniServer.Accounts.{User, Customization} - alias Helper.ORM - # ... - # TODO: Constants + + @default_customization get_config(:customization, :all) |> Enum.into(%{}) + + @doc """ + get user's customization, if not have, return default customization + """ + def get_customization(%User{id: user_id}) do + case ORM.find_by(Customization, user_id: user_id) do + {:ok, customization} -> {:ok, Map.merge(@default_customization, customization)} + {:error, _} -> {:ok, @default_customization} + end + end @doc """ add custom setting to user """ # for map_size # see https://stackoverflow.com/questions/33248816/pattern-match-function-against-empty-map - def add_custom_setting(%User{} = _user, map) when map_size(map) == 0 do + def set_customization(%User{} = _user, map) when map_size(map) == 0 do {:error, "AccountCustomization: invalid option or not purchased"} end - def add_custom_setting(%User{} = user, map) when is_map(map) do - valid? = map |> Map.keys() |> Enum.all?(&can_set?(user, &1, :boolean)) + def set_customization(%User{} = user, map) when is_map(map) do + map = map |> map_atom_value(:string) + + valid? = + map + |> Map.keys() + |> Enum.all?(&can_set?(user, &1, :boolean)) case valid? do true -> @@ -29,7 +48,7 @@ defmodule MastaniServer.Accounts.Delegate.Customization do end end - def add_custom_setting(%User{} = user, key, value \\ true) do + def set_customization(%User{} = user, key, value \\ true) do with {:ok, key} <- can_set?(user, key) do attrs = Map.put(%{user_id: user.id}, key, value) Customization |> ORM.upsert_by([user_id: user.id], attrs) @@ -61,7 +80,14 @@ defmodule MastaniServer.Accounts.Delegate.Customization do # sidebar_layout -- user can arrange subscribed community index """ def valid_custom_items(:free) do - [:theme, :sidebar_layout] + [ + :sidebar_layout, + :banner_layout, + :contents_layout, + :content_divider, + :mark_viewed, + :display_density + ] end @doc """ @@ -71,6 +97,6 @@ defmodule MastaniServer.Accounts.Delegate.Customization do def valid_custom_items(:advance) do # NOTE: :brainwash_free aka. "ads_free" # use brainwash to avoid brower-block-plugins - [:brainwash_free, :community_chart] + [:theme, :brainwash_free, :community_chart] end end diff --git a/lib/mastani_server_web/resolvers/accounts_resolver.ex b/lib/mastani_server_web/resolvers/accounts_resolver.ex index 52af8ae7b..9f7fd0d0d 100644 --- a/lib/mastani_server_web/resolvers/accounts_resolver.ex +++ b/lib/mastani_server_web/resolvers/accounts_resolver.ex @@ -45,6 +45,22 @@ defmodule MastaniServerWeb.Resolvers.Accounts do Accounts.github_signin(github_user, remote_ip) end + def get_customization(_root, _args, %{context: %{cur_user: cur_user}}) do + Accounts.get_customization(cur_user) + end + + # def set_customization(_root, ~m(user_id customization)a, %{context: %{cur_user: cur_user}}) do + # Accounts.set_customization(%User{id: user_id}, customization) + # end + + def set_customization(_root, ~m(customization)a, %{context: %{cur_user: cur_user}}) do + Accounts.set_customization(cur_user, customization) + end + + def set_customization(_root, _args, _info) do + {:error, [message: "need login", code: ecode(:account_login)]} + end + def list_favorite_categories(_root, %{filter: filter}, %{context: %{cur_user: cur_user}}) do Accounts.list_favorite_categories(cur_user, %{private: true}, filter) end diff --git a/lib/mastani_server_web/schema/account/account_misc.ex b/lib/mastani_server_web/schema/account/account_misc.ex index a78438aeb..933f4957f 100644 --- a/lib/mastani_server_web/schema/account/account_misc.ex +++ b/lib/mastani_server_web/schema/account/account_misc.ex @@ -52,6 +52,28 @@ defmodule MastaniServerWeb.Schema.Account.Misc do sscial_fields() end + enum :cus_banner_layout_num do + value(:digest) + value(:brief) + end + + enum :cus_contents_layout_num do + value(:digest) + value(:list) + end + + input_object :customization_input do + field(:theme, :string) + field(:community_chart, :boolean) + field(:brainwash_free, :boolean) + + field(:banner_layout, :cus_banner_layout_num, default_value: :digest) + field(:contents_layout, :cus_contents_layout_num, default_value: :digest) + field(:content_divider, :boolean) + field(:mark_viewed, :boolean) + field(:display_density, :string, default_value: "20") + end + # see: https://github.com/absinthe-graphql/absinthe/issues/206 # https://github.com/absinthe-graphql/absinthe/wiki/Scalar-Recipes scalar :json, name: "Json" do diff --git a/lib/mastani_server_web/schema/account/account_mutations.ex b/lib/mastani_server_web/schema/account/account_mutations.ex index bdd081cbd..5f92625d9 100644 --- a/lib/mastani_server_web/schema/account/account_mutations.ex +++ b/lib/mastani_server_web/schema/account/account_mutations.ex @@ -97,6 +97,14 @@ defmodule MastaniServerWeb.Schema.Account.Mutations do resolve(&R.Accounts.unset_favorites/3) end + @desc "set user's customization" + field :set_customization, :user do + arg(:user_id, :id) + arg(:customization, non_null(:customization_input)) + + resolve(&R.Accounts.set_customization/3) + end + @desc "mark a mention as read" field :mark_mention_read, :status do arg(:id, non_null(:id)) diff --git a/lib/mastani_server_web/schema/account/account_types.ex b/lib/mastani_server_web/schema/account/account_types.ex index 189def474..8dc1f492b 100644 --- a/lib/mastani_server_web/schema/account/account_types.ex +++ b/lib/mastani_server_web/schema/account/account_types.ex @@ -44,6 +44,11 @@ defmodule MastaniServerWeb.Schema.Account.Types do field(:github_profile, :github_profile, resolve: dataloader(Accounts, :github_profile)) field(:achievement, :achievement, resolve: dataloader(Accounts, :achievement)) + field(:customization, :customization) do + middleware(M.Authorize, :login) + resolve(&R.Accounts.get_customization/3) + end + field(:education_backgrounds, list_of(:education_background)) field(:work_backgrounds, list_of(:work_background)) @@ -282,6 +287,19 @@ defmodule MastaniServerWeb.Schema.Account.Types do end end + # field(:sidebar_layout, :map) + object :customization do + field(:theme, :string) + field(:community_chart, :boolean) + field(:brainwash_free, :boolean) + + field(:banner_layout, :string) + field(:contents_layout, :string) + field(:content_divider, :boolean) + field(:mark_viewed, :boolean) + field(:display_density, :string) + end + object :github_profile do field(:id, :id) field(:github_id, :string) diff --git a/priv/repo/migrations/20181103023157_add_more_customization.exs b/priv/repo/migrations/20181103023157_add_more_customization.exs new file mode 100644 index 000000000..13bce2ce2 --- /dev/null +++ b/priv/repo/migrations/20181103023157_add_more_customization.exs @@ -0,0 +1,13 @@ +defmodule MastaniServer.Repo.Migrations.AddMoreCustomization do + use Ecto.Migration + + def change do + alter table(:customizations) do + add(:banner_layout, :string) + add(:contents_layout, :string) + add(:content_divider, :boolean) + add(:mark_viewed, :boolean) + add(:display_density, :string) + end + end +end diff --git a/priv/repo/migrations/20181103031609_alter_customization_theme.exs b/priv/repo/migrations/20181103031609_alter_customization_theme.exs new file mode 100644 index 000000000..44fc64c27 --- /dev/null +++ b/priv/repo/migrations/20181103031609_alter_customization_theme.exs @@ -0,0 +1,10 @@ +defmodule MastaniServer.Repo.Migrations.AlterCustomizationTheme do + use Ecto.Migration + + def change do + alter table(:customizations) do + remove(:theme) + add(:theme, :string) + end + end +end diff --git a/test/mastani_server/accounts/customization_test.exs b/test/mastani_server/accounts/customization_test.exs index 3b72bd498..68d8b9a9c 100644 --- a/test/mastani_server/accounts/customization_test.exs +++ b/test/mastani_server/accounts/customization_test.exs @@ -12,39 +12,46 @@ defmodule MastaniServer.Test.Accounts.Customization do end describe "[user customization]" do + @tag :wip test "user can have default customization without payment", ~m(user)a do - {:ok, result} = Accounts.add_custom_setting(user, :theme, true) - assert result.theme == true + {:ok, result} = Accounts.set_customization(user, :banner_layout, "digest") + assert result.banner_layout == "digest" - {:ok, result} = Accounts.add_custom_setting(user, :theme) - assert result.theme == true - - {:error, _result} = Accounts.add_custom_setting(user, :non_exsit, true) + {:error, _result} = Accounts.set_customization(user, :non_exsit, true) end + @tag :wip test "user set advance customization without payment fails", ~m(user)a do - {:error, _result} = Accounts.add_custom_setting(user, :non_exsit, true) - {:error, _result} = Accounts.add_custom_setting(user, :brainwash_free, true) + {:error, _result} = Accounts.set_customization(user, :non_exsit, true) + {:error, _result} = Accounts.set_customization(user, :brainwash_free, true) end + @tag :wip test "user can set advance customization after pay for it", ~m(user)a do - {:error, _result} = Accounts.add_custom_setting(user, :brainwash_free, true) + {:error, _result} = Accounts.set_customization(user, :brainwash_free, true) {:ok, _result} = Accounts.purchase_service(user, :brainwash_free) - {:ok, _result} = Accounts.add_custom_setting(user, :brainwash_free, true) + {:ok, _result} = Accounts.set_customization(user, :brainwash_free, true) end + @tag :wip test "user can set multiable customization at once", ~m(user)a do {:ok, result} = - Accounts.add_custom_setting(user, %{theme: true, sidebar_layout: %{hello: :world}}) + Accounts.set_customization(user, %{ + content_divider: true, + sidebar_layout: %{hello: :world} + }) - assert result.theme == true + assert result.content_divider == true assert result.sidebar_layout == %{hello: :world} - assert {:error, _result} = Accounts.add_custom_setting(user, %{theme: true, no_exsit: true}) - assert {:error, _result} = Accounts.add_custom_setting(user, %{}) + assert {:error, _result} = + Accounts.set_customization(user, %{content_divider: true, no_exsit: true}) + + assert {:error, _result} = Accounts.set_customization(user, %{}) end + @tag :wip test "user can purchase multiable items at once", ~m(user)a do {:ok, result} = Accounts.purchase_service(user, %{brainwash_free: true, community_chart: true}) diff --git a/test/mastani_server_web/mutation/accounts/customization_test.exs b/test/mastani_server_web/mutation/accounts/customization_test.exs new file mode 100644 index 000000000..b4a08edad --- /dev/null +++ b/test/mastani_server_web/mutation/accounts/customization_test.exs @@ -0,0 +1,82 @@ +defmodule MastaniServer.Test.Mutation.Account.Customization do + use MastaniServer.TestTools + + # alias MastaniServer.{Accounts} + # alias Helper.ORM + + setup do + {:ok, user} = db_insert(:user) + + user_conn = simu_conn(:user, user) + guest_conn = simu_conn(:guest) + + {:ok, ~m(user_conn guest_conn user)a} + end + + describe "[account customization mutation]" do + @query """ + mutation($userId: ID, $customization: CustomizationInput!) { + setCustomization( userId: $userId, customization: $customization) { + id + customization { + bannerLayout + contentDivider + markViewed + displayDensity + } + } + } + """ + @tag :wip2 + test "user can set customization", ~m(user_conn user)a do + ownd_conn = simu_conn(:user, user) + + variables = %{ + customization: %{ + bannerLayout: "BRIEF", + contentDivider: true, + markViewed: false, + displayDensity: "25" + } + } + + result = user_conn |> mutation_result(@query, variables, "setCustomization") + + assert result["customization"]["bannerLayout"] == "brief" + assert result["customization"]["contentDivider"] == true + assert result["customization"]["markViewed"] == false + assert result["customization"]["displayDensity"] == "25" + end + + @tag :wip2 + test "user set customization with invalid attr fails", ~m(user_conn user)a do + ownd_conn = simu_conn(:user, user) + + variables1 = %{ + customization: %{ + bannerLayout: "OTHER" + } + } + + variables2 = %{ + customization: %{ + contentsLayout: "OTHER" + } + } + + assert user_conn |> mutation_get_error?(@query, variables1) + assert user_conn |> mutation_get_error?(@query, variables2) + end + + @tag :wip2 + test "unlogin user set customization fails", ~m(guest_conn)a do + variables = %{ + customization: %{ + bannerLayout: "DIGEST" + } + } + + assert guest_conn |> mutation_get_error?(@query, variables, ecode(:account_login)) + end + end +end diff --git a/test/mastani_server_web/query/accounts/customization_test.exs b/test/mastani_server_web/query/accounts/customization_test.exs new file mode 100644 index 000000000..7e288ac8c --- /dev/null +++ b/test/mastani_server_web/query/accounts/customization_test.exs @@ -0,0 +1,52 @@ +defmodule MastaniServer.Test.Query.Account.Customization do + use MastaniServer.TestTools + import Helper.Utils, only: [get_config: 2] + alias MastaniServer.{Accounts, CMS} + + alias Helper.ORM + + @default_customization get_config(:customization, :all) |> Enum.into(%{}) + + setup do + {:ok, user} = db_insert(:user) + guest_conn = simu_conn(:guest) + user_conn = simu_conn(:user, user) + + {:ok, ~m(user_conn guest_conn user)a} + end + + describe "[account customization]" do + @query """ + query { + user { + id + nickname + customization { + theme + communityChart + brainwashFree + bannerLayout + contentsLayout + contentDivider + markViewed + displayDensity + } + } + } + """ + @tag :wip + test "user can have default customization configs", ~m(user_conn user)a do + results = user_conn |> query_result(@query, %{}, "user") + + assert results["id"] == to_string(user.id) + + assert results["customization"]["theme"] == @default_customization |> Map.get(:theme) + + assert results["customization"]["bannerLayout"] == + @default_customization |> Map.get(:banner_layout) + + assert results["customization"]["contentsLayout"] == + @default_customization |> Map.get(:contents_layout) + end + end +end From 05058d2a08f6074d39d163654620cafa9af0b8d7 Mon Sep 17 00:00:00 2001 From: mydearxym Date: Sun, 4 Nov 2018 22:04:38 +0800 Subject: [PATCH 128/129] fix(c11n): filter nil when user has no c11n & clean up --- .../accounts/delegates/customization.ex | 12 ++++++++++-- test/mastani_server/accounts/customization_test.exs | 5 ----- .../mutation/accounts/customization_test.exs | 3 --- .../query/accounts/customization_test.exs | 1 - 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/mastani_server/accounts/delegates/customization.ex b/lib/mastani_server/accounts/delegates/customization.ex index f42785c82..e5a9f6608 100644 --- a/lib/mastani_server/accounts/delegates/customization.ex +++ b/lib/mastani_server/accounts/delegates/customization.ex @@ -16,8 +16,12 @@ defmodule MastaniServer.Accounts.Delegate.Customization do """ def get_customization(%User{id: user_id}) do case ORM.find_by(Customization, user_id: user_id) do - {:ok, customization} -> {:ok, Map.merge(@default_customization, customization)} - {:error, _} -> {:ok, @default_customization} + {:ok, customization} -> + customization = customization |> Map.from_struct() |> filter_nil_value + {:ok, Map.merge(@default_customization, customization)} + + {:error, _} -> + {:ok, @default_customization} end end @@ -99,4 +103,8 @@ defmodule MastaniServer.Accounts.Delegate.Customization do # use brainwash to avoid brower-block-plugins [:theme, :brainwash_free, :community_chart] end + + defp filter_nil_value(map) do + for {k, v} <- map, !is_nil(v), into: %{}, do: {k, v} + end end diff --git a/test/mastani_server/accounts/customization_test.exs b/test/mastani_server/accounts/customization_test.exs index 68d8b9a9c..434929df6 100644 --- a/test/mastani_server/accounts/customization_test.exs +++ b/test/mastani_server/accounts/customization_test.exs @@ -12,7 +12,6 @@ defmodule MastaniServer.Test.Accounts.Customization do end describe "[user customization]" do - @tag :wip test "user can have default customization without payment", ~m(user)a do {:ok, result} = Accounts.set_customization(user, :banner_layout, "digest") assert result.banner_layout == "digest" @@ -20,13 +19,11 @@ defmodule MastaniServer.Test.Accounts.Customization do {:error, _result} = Accounts.set_customization(user, :non_exsit, true) end - @tag :wip test "user set advance customization without payment fails", ~m(user)a do {:error, _result} = Accounts.set_customization(user, :non_exsit, true) {:error, _result} = Accounts.set_customization(user, :brainwash_free, true) end - @tag :wip test "user can set advance customization after pay for it", ~m(user)a do {:error, _result} = Accounts.set_customization(user, :brainwash_free, true) {:ok, _result} = Accounts.purchase_service(user, :brainwash_free) @@ -34,7 +31,6 @@ defmodule MastaniServer.Test.Accounts.Customization do {:ok, _result} = Accounts.set_customization(user, :brainwash_free, true) end - @tag :wip test "user can set multiable customization at once", ~m(user)a do {:ok, result} = Accounts.set_customization(user, %{ @@ -51,7 +47,6 @@ defmodule MastaniServer.Test.Accounts.Customization do assert {:error, _result} = Accounts.set_customization(user, %{}) end - @tag :wip test "user can purchase multiable items at once", ~m(user)a do {:ok, result} = Accounts.purchase_service(user, %{brainwash_free: true, community_chart: true}) diff --git a/test/mastani_server_web/mutation/accounts/customization_test.exs b/test/mastani_server_web/mutation/accounts/customization_test.exs index b4a08edad..a5ca68f83 100644 --- a/test/mastani_server_web/mutation/accounts/customization_test.exs +++ b/test/mastani_server_web/mutation/accounts/customization_test.exs @@ -27,7 +27,6 @@ defmodule MastaniServer.Test.Mutation.Account.Customization do } } """ - @tag :wip2 test "user can set customization", ~m(user_conn user)a do ownd_conn = simu_conn(:user, user) @@ -48,7 +47,6 @@ defmodule MastaniServer.Test.Mutation.Account.Customization do assert result["customization"]["displayDensity"] == "25" end - @tag :wip2 test "user set customization with invalid attr fails", ~m(user_conn user)a do ownd_conn = simu_conn(:user, user) @@ -68,7 +66,6 @@ defmodule MastaniServer.Test.Mutation.Account.Customization do assert user_conn |> mutation_get_error?(@query, variables2) end - @tag :wip2 test "unlogin user set customization fails", ~m(guest_conn)a do variables = %{ customization: %{ diff --git a/test/mastani_server_web/query/accounts/customization_test.exs b/test/mastani_server_web/query/accounts/customization_test.exs index 7e288ac8c..9c01f7e55 100644 --- a/test/mastani_server_web/query/accounts/customization_test.exs +++ b/test/mastani_server_web/query/accounts/customization_test.exs @@ -34,7 +34,6 @@ defmodule MastaniServer.Test.Query.Account.Customization do } } """ - @tag :wip test "user can have default customization configs", ~m(user_conn user)a do results = user_conn |> query_result(@query, %{}, "user") From 448a2fd6d1a8800a10fa393f2db15f8c53f09d62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Mon, 5 Nov 2018 21:16:53 +0000 Subject: [PATCH 129/129] chore(deps): bump sentry from 6.4.1 to 7.0.2 Bumps [sentry](https://github.com/getsentry/sentry-elixir) from 6.4.1 to 7.0.2. - [Release notes](https://github.com/getsentry/sentry-elixir/releases) - [Changelog](https://github.com/getsentry/sentry-elixir/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-elixir/commits) Signed-off-by: dependabot[bot] --- mix.exs | 2 +- mix.lock | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/mix.exs b/mix.exs index 4509350f4..a49432a46 100644 --- a/mix.exs +++ b/mix.exs @@ -88,7 +88,7 @@ defmodule MastaniServer.Mixfile do {:credo, "~> 0.10.0", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.0.0-rc.2", only: [:dev, :mock], runtime: false}, {:excoveralls, "~> 0.8", only: :test}, - {:sentry, "~> 6.4"}, + {:sentry, "~> 7.0"}, {:recase, "~> 0.3.0"} ] end diff --git a/mix.lock b/mix.lock index ca9e78322..20b757606 100644 --- a/mix.lock +++ b/mix.lock @@ -6,7 +6,7 @@ "argon2_elixir": {:hex, :argon2_elixir, "1.2.14", "0fc4bfbc1b7e459954987d3d2f3836befd72d63f3a355e3978f5005dd6e80816", [], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, - "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, + "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "comeonin": {:hex, :comeonin, "4.0.3", "4e257dcb748ed1ca2651b7ba24fdbd1bd24efd12482accf8079141e3fda23a10", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, @@ -28,8 +28,8 @@ "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, "guardian": {:hex, :guardian, "1.1.1", "be14c4007eaf05268251ae114030cb7237ed9a9631c260022f020164ff4ed733", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "inch_ex": {:hex, :inch_ex, "1.0.1", "1f0af1a83cec8e56f6fc91738a09c838e858db3d78ef5f2ec040fe4d5a62dabf", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, @@ -37,11 +37,12 @@ "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"}, - "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_ecto": {:hex, :phoenix_ecto, "3.4.0", "91cd39427006fe4b5588d69f0941b9c3d3d8f5e6477c563a08379de7de2b0c58", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.0", "d55e25ff1ff8ea2f9964638366dfd6e361c52dedfd50019353598d11d4441d14", [:mix], [], "hexpm"}, - "plug": {:hex, :plug, "1.6.3", "43088304337b9e8b8bd22a0383ca2f633519697e4c11889285538148f42cbc5e", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.1", "6668d787e602981f24f17a5fbb69cc98f8ab085114ebfac6cc36e10a90c8e93c", [:mix], [], "hexpm"}, + "plug": {:hex, :plug, "1.7.1", "8516d565fb84a6a8b2ca722e74e2cd25ca0fc9d64f364ec9dbec09d33eb78ccd", [], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, + "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [], [], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, @@ -50,12 +51,12 @@ "recase": {:hex, :recase, "0.3.0", "a3a6b2bfc9a1c3047b6f37d49ea52027ea59fd256505254b8e9d63c68d09ab89", [:mix], [], "hexpm"}, "scrivener": {:git, "https://github.com/mastani-stack/scrivener", "dc603c5cdf884c4fe33b2b09d5672ab6be3e2c14", []}, "scrivener_ecto": {:git, "https://github.com/mastani-stack/scrivener_ecto", "e7d2f287c9189f2aebf478724b6c276694411c92", []}, - "sentry": {:hex, :sentry, "6.4.1", "882287f1f3167dc4794865124977e2d88878d51d19930c0d0e7cc3a663a4a181", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "sentry": {:hex, :sentry, "7.0.2", "0795e1e3955c9d1b9c35484dcba8169a76cff20cbc86de97327a4fe454515a85", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 1.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "short_maps": {:hex, :short_maps, "0.1.2", "a7c2bfd91179cdbdfe90e74a023992335d116982fa672612c74776b2e9257a7b", [:mix], [], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, "tesla": {:hex, :tesla, "0.10.0", "e588c7e7f1c0866c81eeed5c38f02a4a94d6309eede336c1e6ca08b0a95abd3f", [:mix], [{:exjsx, ">= 0.1.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "timex": {:hex, :timex, "3.2.1", "639975eac45c4c08c2dbf7fc53033c313ff1f94fad9282af03619a3826493612", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "tzdata": {:hex, :tzdata, "0.5.16", "13424d3afc76c68ff607f2df966c0ab4f3258859bbe3c979c9ed1606135e7352", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"}, }