Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/google_crawler/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ defmodule GoogleCrawler.Accounts.User do
field :password, :string, virtual: true
field :password_confirmation, :string, virtual: true

has_many :keywords, GoogleCrawler.Search.Keyword

timestamps()
end

Expand Down
18 changes: 10 additions & 8 deletions lib/google_crawler/search.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ defmodule GoogleCrawler.Search do
alias GoogleCrawler.Search.KeywordFile

@doc """
Returns the list of keywords.
Returns the list of keywords belongs to the given user.

## Examples

iex> list_keywords()
iex> list_user_keywords(user)
[%Keyword{}, ...]

"""
def list_keywords do
Repo.all(Keyword)
def list_user_keywords(user) do
Keyword
|> where(user_id: ^user.id)
|> Repo.all()
end

@doc """
Expand All @@ -43,15 +45,15 @@ defmodule GoogleCrawler.Search do

## Examples

iex> create_keyword(%{field: value})
iex> create_keyword(%{field: value}, %User{})
{:ok, %Keyword{}}

iex> create_keyword(%{field: bad_value})
iex> create_keyword(%{field: bad_value}, %User{})
{:error, %Ecto.Changeset{}}

"""
def create_keyword(attrs \\ %{}) do
%Keyword{}
def create_keyword(attrs \\ %{}, user) do
Ecto.build_assoc(user, :keywords)
|> Keyword.changeset(attrs)
|> Repo.insert()
end
Expand Down
6 changes: 4 additions & 2 deletions lib/google_crawler/search/keyword.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ defmodule GoogleCrawler.Search.Keyword do
schema "keywords" do
field :keyword, :string

belongs_to :user, GoogleCrawler.Accounts.User

timestamps()
end

def changeset(keyword, attrs \\ %{}) do
keyword
|> cast(attrs, [:keyword])
|> validate_required([:keyword])
|> cast(attrs, [:keyword, :user_id])
|> validate_required([:keyword, :user_id])
end
end
4 changes: 3 additions & 1 deletion lib/google_crawler_web/controllers/dashboard_controller.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
defmodule GoogleCrawlerWeb.DashboardController do
use GoogleCrawlerWeb, :controller

alias GoogleCrawler.Search
alias GoogleCrawler.Search.KeywordFile

def index(conn, _params) do
keywords = Search.list_user_keywords(conn.assigns.current_user)
changeset = KeywordFile.changeset(%KeywordFile{})

render(conn, "index.html", changeset: changeset)
render(conn, "index.html", keywords: keywords, changeset: changeset)
end
end
41 changes: 38 additions & 3 deletions lib/google_crawler_web/controllers/upload_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,49 @@ defmodule GoogleCrawlerWeb.UploadController do

if changeset.valid? do
file = get_change(changeset, :file, nil)
result = Search.parse_keywords_from_file!(file.path, file.content_type)

# TODO: Save these keywords and triggers the task to google search for each keyword
text(conn, result |> Enum.map(fn keyword -> List.first(keyword) end) |> Enum.join(", "))
Search.parse_keywords_from_file!(file.path, file.content_type)
|> create_and_trigger_google_search(conn)
|> put_error_flash_for_failed_keywords(conn)
|> redirect(to: Routes.dashboard_path(conn, :index))
else
conn
|> put_flash(:error, gettext("Invalid file, please select again."))
|> redirect(to: Routes.dashboard_path(conn, :index))
end
end

# TODO: Trigger the scrapper background worker
defp create_and_trigger_google_search(csv_result, conn) do
csv_result
|> Stream.map(fn keyword_row -> List.first(keyword_row) end)
|> Stream.map(fn keyword -> %{keyword: keyword} end)
|> Enum.map(&Search.create_keyword(&1, conn.assigns.current_user))
end

defp put_error_flash_for_failed_keywords(create_result, conn) do
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm displaying some error message when some of the keywords cannot be created but I'm not sure how to test it 🤔💭

failed_keywords = failed_keywords(create_result)

if length(failed_keywords) > 0 do
conn
|> put_flash(
:error,
gettext("Some keywords could not be created: %{failed_keywords}",
failed_keywords: Enum.join(failed_keywords, ", ")
)
)
else
conn
end
end

defp failed_keywords(create_result) do
create_result
|> Enum.filter(&match?({:error, _}, &1))
|> Enum.map(fn error_tuple ->
error_tuple
|> elem(1)
|> get_change(:keyword, nil)
end)
end
end
14 changes: 12 additions & 2 deletions lib/google_crawler_web/templates/dashboard/index.html.eex
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
<%= render GoogleCrawlerWeb.KeywordView, "_form.html", assigns %>
<section>
<%= render GoogleCrawlerWeb.KeywordView, "_form.html", assigns %>
</section>
<hr>
<section>
<h3><%= gettext("Keywords") %></h3>
<p><%= gettext("You don't have any keywords.") %></p>
<%= if length(@keywords) == 0 do %>
<p><%= gettext("You don't have any keywords.") %></p>
<% else %>
<ul>
<%= for keyword <- @keywords do %>
<li><%= keyword.keyword %></li>
<% end %>
</ul>
<% end %>
</section>
18 changes: 8 additions & 10 deletions lib/google_crawler_web/templates/keyword/_form.html.eex
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
<section>
<h3><%= gettext("Upload your keyword file (.csv)") %></h3>
<p><%= gettext("📝 Please put one keyword per line") %></p>
<%= form_for @changeset, Routes.upload_path(@conn, :create), [multipart: true], fn f -> %>
<%= label f, :file %>
<%= file_input f, :file, required: true %>
<%= error_tag f, :file %>
<h3><%= gettext("Upload your keyword file (.csv)") %></h3>
<p><%= gettext("📝 Please put one keyword per line") %></p>
<%= form_for @changeset, Routes.upload_path(@conn, :create), [multipart: true], fn f -> %>
<%= label f, :file %>
<%= file_input f, :file, required: true, accept: "text/csv" %>
<%= error_tag f, :file %>

<%= submit gettext("Upload") %>
<% end %>
</section>
<%= submit gettext("Upload") %>
<% end %>
11 changes: 11 additions & 0 deletions priv/repo/migrations/20200330130512_add_user_id_to_keywords.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule GoogleCrawler.Repo.Migrations.AddUserIdToKeywords do
use Ecto.Migration

def change do
alter table(:keywords) do
add :user_id, references(:users, on_delete: :delete_all), null: false
end

create index(:keywords, [:user_id])
end
end
5 changes: 3 additions & 2 deletions test/factories/keyword_factory.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule GoogleCrawler.KeywordFactory do
alias GoogleCrawler.Search
alias GoogleCrawler.UserFactory

def default_attrs do
%{
Expand All @@ -11,10 +12,10 @@ defmodule GoogleCrawler.KeywordFactory do
Enum.into(attrs, default_attrs())
end

def create(attrs \\ %{}) do
def create(attrs \\ %{}, user \\ UserFactory.create()) do
keyword_attrs = build_attrs(attrs)

{:ok, keyword} = Search.create_keyword(keyword_attrs)
{:ok, keyword} = Search.create_keyword(keyword_attrs, user)

keyword
end
Expand Down
26 changes: 26 additions & 0 deletions test/google_crawler/search/keyword_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Googlecrawler.Search.KeywordTest do
use GoogleCrawler.DataCase

alias GoogleCrawler.Search.Keyword
alias GoogleCrawler.UserFactory
alias GoogleCrawler.KeywordFactory

describe "changeset" do
test "keyword is required" do
user = UserFactory.create()
attrs = KeywordFactory.build_attrs(%{keyword: "", user: user})
changeset = Keyword.changeset(%Keyword{}, attrs)

refute changeset.valid?
assert %{keyword: ["can't be blank"]} = errors_on(changeset)
end

test "user is required" do
attrs = KeywordFactory.build_attrs()
changeset = Keyword.changeset(%Keyword{}, attrs)

refute changeset.valid?
assert %{user_id: ["can't be blank"]} = errors_on(changeset)
end
end
end
18 changes: 13 additions & 5 deletions test/google_crawler/search_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ defmodule GoogleCrawler.SearchTest do
alias GoogleCrawler.Search
alias GoogleCrawler.Search.Keyword
alias GoogleCrawler.KeywordFactory
alias GoogleCrawler.UserFactory

describe "keywords" do
test "list_keywords/0 returns all keywords" do
keyword = KeywordFactory.create()
test "list_user_keywords/0 returns all keywords" do
user1 = UserFactory.create()
user2 = UserFactory.create()
keyword1 = KeywordFactory.create(%{}, user1)
_keyword2 = KeywordFactory.create(%{}, user2)

user_keywords = Search.list_user_keywords(user1)

assert Search.list_keywords() |> Enum.map(&Map.get(&1, :keyword)) == [keyword.keyword]
assert user_keywords |> Enum.map(&Map.get(&1, :keyword)) == [keyword1.keyword]
end

test "get_keyword/1 returns the keyword with given id" do
Expand All @@ -19,16 +25,18 @@ defmodule GoogleCrawler.SearchTest do
end

test "create_keyword/1 with valid data creates a keyword" do
user = UserFactory.create()
keyword_attrs = KeywordFactory.build_attrs(%{keyword: "elixir"})

assert {:ok, %Keyword{} = keyword} = Search.create_keyword(keyword_attrs)
assert {:ok, %Keyword{} = keyword} = Search.create_keyword(keyword_attrs, user)
assert keyword.keyword == "elixir"
end

test "create_keyword/1 with invalid data returns error changeset" do
user = UserFactory.create()
keyword_attrs = KeywordFactory.build_attrs(%{keyword: ""})

assert {:error, %Ecto.Changeset{}} = Search.create_keyword(keyword_attrs)
assert {:error, %Ecto.Changeset{}} = Search.create_keyword(keyword_attrs, user)
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ defmodule GoogleCrawlerWeb.UploadControllerTest do
use GoogleCrawlerWeb.ConnCase

alias GoogleCrawler.UserFactory
alias GoogleCrawler.Repo
alias GoogleCrawler.Search.Keyword

test "create/2 renders csv content as text if the keyword file is valid", %{conn: conn} do
test "create/2 creates keywords and redirects to the user dashboard if the keyword file is valid",
%{conn: conn} do
user = UserFactory.create()
upload_file = upload_file_fixture("keyword_files/valid_keyword.csv")

conn =
build_authenticated_conn(user)
|> post(Routes.upload_path(conn, :create), %{keyword_file: %{file: upload_file}})

assert text_response(conn, 200) == "elixir, ruby, javascript"
keywords = Repo.all(Keyword) |> Enum.map(&Map.get(&1, :keyword))
assert keywords == ["elixir", "ruby", "javascript"]
assert redirected_to(conn) == Routes.dashboard_path(conn, :index)
end

test "create/2 raises error if the file is failed to parse" do
Expand Down