Organize test helper

Organize test helper

defmodule WeberHelperHtmlTest do
  use ExUnit.Case

  import Weber.Helper.Html

  test "Create a tag without attributes within text" do
    html = tag(:p, "test")
    assert(html == "
test
")
  end

  test "Create tag with attributes" do
    html = tag(:p, "test", [class: "test_class", id: "test_id"])
    assert(html == "
test
")
  end

  test "Create tag with attributes boolean" do
    html = tag(:p, "test", [class: "test_class", id: "test_id", required: true])
    assert(html == "
test
")
  end

  test "Create tag with another tags inside" do
    html = tag(:div) do
      tag(:p, "test", [class: "test"])
    end
    assert(html == "

test

")
  end

  test "Create tag with attributes and another tags inside" do
    html = tag(:div, [class: "div_test", id: "id_test"]) do
      tag(:span, [class: "span_class"]) do
        tag(:p, "test", [class: "test"])
      end
    end

    assert(html == "

test

")
  end

  test "Create tags without end" do
    html = tag(:img, [src: "path/to/file.png"])
    assert(html == "")
  end
end


Refactor

Refactor

defmodule Xee.HostView do
  use Xee.Web, :view
end


fix fix

fix fix

defmodule Democracy.NumberFormat do
	use Phoenix.HTML

	def number_format(x) do
		raw(format_number(x))
	end

	def number_format_simple(x) do
		s = :erlang.float_to_binary(x, [:compact, { :decimals, 2 }])
		|> String.trim_trailing(".0")
		raw(s)
	end

	def for_choice_format(for_choice, choice_type) do
		if choice_type == "probability" do
			case for_choice do
				0.0 -> raw('NEGATIVE')
				1.0 -> raw('POSITIVE')
				x -> raw("#{format_number(x * 100)}%")
			end
		else
			raw(format_number(for_choice))
		end
	end

	def score_format(score, choice_type) do
		if choice_type == "probability" do
			case score do
				0.0 -> raw('FALSE')
				1.0 -> raw('TRUE')
				x -> raw("#{format_number(x * 100)}%")
			end
		else
			raw(format_number(score))
		end
	end

	def score_format(score) do
		score_format(score, "probability")
	end

	def format_number(x) do
		x = x * 1.0
		{value, suffix} = cond do
			x > 999999999.999999 ->
				{x / 1000000000, " x 109"}
			x > 999999.999999 ->
				{x / 1000000, " x 106"}
			true ->
				{x, ""}
		end

		s = (:erlang.float_to_binary(value, [:compact, { :decimals, 2 }])
		|> String.trim_trailing(".0"))  <> suffix
	end
end

defmodule Democracy.NumberFormat do
	use Phoenix.HTML

	def number_format(x) do
		raw(format_number(x))
	end

	def number_format_simple(x) do
		s = :erlang.float_to_binary(x, [:compact, { :decimals, 2 }])
		|> String.trim_trailing(".0")
		raw(s)
	end

	def for_choice_format(for_choice, choice_type) do
		if choice_type == "probability" do
			case for_choice do
				0.0 -> raw('NEGATIVE')
				1.0 -> raw('POSITIVE')
				x -> raw("#{format_number(x * 100)}%")
			end
		else
			raw(format_number(for_choice))
		end
	end

	def score_format(score, choice_type) do
		if choice_type == "probability" do
			case score do
				0.0 -> raw('FALSE')
				1.0 -> raw('TRUE')
				x -> raw("#{format_number(x * 100)}%")
			end
		else
			raw(format_number(score))
		end
	end

	def score_format(score) do
		score_format(score, "probability")
	end

	def format_number(x) do
		x = x * 1.0
		{value, suffix} = cond do
			x > 999999999.999999 ->
				{x / 1000000000, " x 109"}
			x > 999999.999999 ->
				{x / 1000000, " x 106"}
			true ->
				{x, ""}
		end

		s = (:erlang.float_to_binary(value, [:compact, { :decimals, 2 }])
		|> String.trim_trailing(".0", ""))  <> suffix
	end
end


When checking for koans, make sure we talk about loaded modules first.

defmodule Runner do
  use GenServer

  def koan?(koan) do
    case Code.ensure_loaded(koan) do
      {:module, _} -> Keyword.has_key?(koan.__info__(:functions), :all_koans)
      _ -> false
    end
  end

  def modules do
    {:ok, modules} = :application.get_key(:elixir_koans, :modules)

    modules
    |> Stream.map(&(&1.module_info |> get_in([:compile, :source])))
    |> Stream.map(&to_string/1)  # Paths are charlists
    |> Stream.zip(modules)
    |> Stream.filter(fn {_path, mod} -> koan?(mod) end)
    |> Stream.map(fn {path, mod} -> {path_to_number(path), mod} end)
    |> Enum.sort_by(fn {number, _mod} -> number end)
    |> Enum.map(fn {_number, mod} -> mod end)
  end

  @koan_path_pattern ~r/lib\/koans\/(\d+)_\w+.ex$/

  def path_to_number(path) do
    [_path, number] = Regex.run(@koan_path_pattern, path)
    String.to_integer(number)
  end

  def modules_to_run(start_module), do: Enum.drop_while(modules(), &(&1 != start_module))

  def start_link do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  def handle_cast({:run, modules}, _) do
    flush()
    send(self(), :run_modules)
    {:noreply, modules}
  end

  def handle_info(:run_modules, []) do
    {:noreply, []}
  end

  def handle_info(:run_modules, [module | rest]) do
    Display.clear_screen()

    case run_module(module) do
      :passed ->
        send(self(), :run_modules)
        {:noreply, rest}

      _ ->
        {:noreply, []}
    end
  end

  def run(modules) do
    GenServer.cast(__MODULE__, {:run, modules})
  end

  defp run_module(module) do
    module
    |> Execute.run_module(&track/3)
    |> display
  end

  defp track(:passed, module, koan), do: Tracker.completed(module, koan)
  defp track(_, _, _), do: nil

  defp display({:failed, error, module, name}) do
    Display.show_failure(error, module, name)
    :failed
  end
  defp display(_), do: :passed

  defp flush do
    receive do
      _ -> flush()
    after
      0 -> :ok
    end
  end
end

defmodule Runner do
  use GenServer

  def koan?(koan) do
    Keyword.has_key?(koan.__info__(:functions), :all_koans)
  end

  def modules do
    {:ok, modules} = :application.get_key(:elixir_koans, :modules)

    modules
    |> Stream.map(&(&1.module_info |> get_in([:compile, :source])))
    |> Stream.map(&to_string/1)  # Paths are charlists
    |> Stream.zip(modules)
    |> Stream.filter(fn {_path, mod} -> koan?(mod) end)
    |> Stream.map(fn {path, mod} -> {path_to_number(path), mod} end)
    |> Enum.sort_by(fn {number, _mod} -> number end)
    |> Enum.map(fn {_number, mod} -> mod end)
  end

  @koan_path_pattern ~r/lib\/koans\/(\d+)_\w+.ex$/

  def path_to_number(path) do
    [_path, number] = Regex.run(@koan_path_pattern, path)
    String.to_integer(number)
  end

  def modules_to_run(start_module), do: Enum.drop_while(modules(), &(&1 != start_module))

  def start_link do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  def handle_cast({:run, modules}, _) do
    flush()
    send(self(), :run_modules)
    {:noreply, modules}
  end

  def handle_info(:run_modules, []) do
    {:noreply, []}
  end

  def handle_info(:run_modules, [module | rest]) do
    Display.clear_screen()

    case run_module(module) do
      :passed ->
        send(self(), :run_modules)
        {:noreply, rest}

      _ ->
        {:noreply, []}
    end
  end

  def run(modules) do
    GenServer.cast(__MODULE__, {:run, modules})
  end

  defp run_module(module) do
    module
    |> Execute.run_module(&track/3)
    |> display
  end

  defp track(:passed, module, koan), do: Tracker.completed(module, koan)
  defp track(_, _, _), do: nil

  defp display({:failed, error, module, name}) do
    Display.show_failure(error, module, name)
    :failed
  end
  defp display(_), do: :passed

  defp flush do
    receive do
      _ -> flush()
    after
      0 -> :ok
    end
  end
end


make dummy depend on distillery

make dummy depend on distillery

defmodule Dummy1.Mixfile do
  use Mix.Project

  def project do
    [app: :dummy1,
     version: "0.1.0",
     elixir: "~> 1.4",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    # Specify extra applications you'll use from Erlang/Elixir
    [extra_applications: [:logger, :edeliver]]
  end

  # Dependencies can be Hex packages:
  #
  #   {:my_dep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    [
      {:edeliver, "~> 1.4.2"},
      {:distillery, "~> 0.9"}, # even though onartsipac already depends on distillery,
        # edeliver complains it cannot detect it.
      {:onartsipac, path: "__ONARTSIPAC_BASE__"} # the path will be replaced in CI
    ]
  end
end

defmodule Dummy1.Mixfile do
  use Mix.Project

  def project do
    [app: :dummy1,
     version: "0.1.0",
     elixir: "~> 1.4",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    # Specify extra applications you'll use from Erlang/Elixir
    [extra_applications: [:logger, :edeliver]]
  end

  # Dependencies can be Hex packages:
  #
  #   {:my_dep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    [
      {:edeliver, "~> 1.4.2"},
      {:onartsipac, path: "__ONARTSIPAC_BASE__"} # the path will be replaced in CI
    ]
  end
end


rename bool funcs in feature runner

rename bool funcs in feature runner

defmodule WhiteBread.Runners.FeatureRunner do

  def run(feature, context, output_pid) do
    %{scenarios: scenarios, background_steps: background_steps} = feature
    results = scenarios
      |> run_all_scenarios_for_context(context, background_steps)
      |> flatten_any_result_lists
      |> output_results(feature, output_pid)

    %{
      successes: results |> Enum.filter(&success?/1),
      failures:  results |> Enum.filter(&failure?/1)
    }
  end

  defp run_all_scenarios_for_context(scenarios, context, background_steps) do
    starting_state = apply(context, :feature_state, [])
    scenarios
      |> Stream.map(&run_scenario(&1,context, background_steps, starting_state))
  end

  defp run_scenario(scenario,context, background_steps, starting_state) do
    result = run(scenario, context, background_steps, starting_state)
    {scenario, result}
  end

  defp flatten_any_result_lists(results) do
    flatten = fn
      ({scenario, results}) when is_list(results) ->
        results |> Enum.map(fn(result) -> {scenario, result} end)
      (single) ->
        [single]
    end
    results |> Stream.flat_map(flatten)
  end

  defp output_results(results, feature, output_pid) do
    send_results = fn({scenario, result}) ->
      send(output_pid, {:scenario_result, result, scenario, feature})
    end
    results
      |> Stream.each(send_results)
      |> Stream.run

    results
  end

  defp success?({_scenario, {success, _}}), do: success == :ok
  defp failure?({_scenario, {success, _}}), do: success == :failed

  defp run(scenario, context, background_steps, starting_state) do
    scenario
      |> WhiteBread.Runners.run(context, background_steps, starting_state)
  end

end

defmodule WhiteBread.Runners.FeatureRunner do

  def run(feature, context, output_pid) do
    %{scenarios: scenarios, background_steps: background_steps} = feature
    results = scenarios
      |> run_all_scenarios_for_context(context, background_steps)
      |> flatten_any_result_lists
      |> output_results(feature, output_pid)

    %{
      successes: results |> Enum.filter(&is_success/1),
      failures:  results |> Enum.filter(&is_failure/1)
    }
  end

  defp run_all_scenarios_for_context(scenarios, context, background_steps) do
    starting_state = apply(context, :feature_state, [])
    scenarios
      |> Stream.map(&run_scenario(&1,context, background_steps, starting_state))
  end

  defp run_scenario(scenario,context, background_steps, starting_state) do
    result = run(scenario, context, background_steps, starting_state)
    {scenario, result}
  end

  defp flatten_any_result_lists(results) do
    flatten = fn
      ({scenario, results}) when is_list(results) ->
        results |> Enum.map(fn(result) -> {scenario, result} end)
      (single) ->
        [single]
    end
    results |> Stream.flat_map(flatten)
  end

  defp output_results(results, feature, output_pid) do
    send_results = fn({scenario, result}) ->
      send(output_pid, {:scenario_result, result, scenario, feature})
    end
    results
      |> Stream.each(send_results)
      |> Stream.run

    results
  end

  defp is_success({_scenario, {success, _}}) do
    success == :ok
  end

  defp is_failure({_scenario, {success, _}}) do
    success == :failed
  end

  defp run(scenario, context, background_steps, starting_state) do
    scenario
      |> WhiteBread.Runners.run(context, background_steps, starting_state)
  end

end


Remove duplicate :metadata key in test

Remove duplicate :metadata key in test

defmodule CodeCorps.StripeService.Adapters.StripePlatformCardTest do
  use ExUnit.Case, async: true

  import CodeCorps.StripeService.Adapters.StripePlatformCardAdapter, only: [to_params: 2]

  @stripe_platform_card %Stripe.Card{
    id: "card_123",
    address_city: nil,
    address_country: nil,
    address_line1: nil,
    address_line1_check: nil,
    address_line2: nil,
    address_state: nil,
    address_zip: nil,
    address_zip_check: nil,
    brand: "Visa",
    country: "US",
    customer: "cus_123",
    cvc_check: "unchecked",
    dynamic_last4: nil,
    exp_month: 11,
    exp_year: 2016,
    funding: "credit",
    last4: "4242",
    metadata: %{},
    name: nil,
    tokenization_method: nil
  }

  @local_map %{
    "id_from_stripe" => "card_123",
    "brand" => "Visa",
    "exp_month" => 11,
    "exp_year" => 2016,
    "last4" => "4242",
    "customer_id_from_stripe" => "cus_123",
    "cvc_check" => "unchecked",
    "name" => nil
  }

  describe "to_params/2" do
    test "converts from stripe map to local properly" do
      test_attributes = %{
        "user_id" => 123,
        "foo" => "bar"
      }
      expected_attributes = %{
        "user_id" => 123,
      }

      {:ok, result} = to_params(@stripe_platform_card, test_attributes)
      expected_map = Map.merge(@local_map, expected_attributes)

      assert result == expected_map
    end
  end
end

defmodule CodeCorps.StripeService.Adapters.StripePlatformCardTest do
  use ExUnit.Case, async: true

  import CodeCorps.StripeService.Adapters.StripePlatformCardAdapter, only: [to_params: 2]

  @stripe_platform_card %Stripe.Card{
    id: "card_123",
    metadata: %{},
    address_city: nil,
    address_country: nil,
    address_line1: nil,
    address_line1_check: nil,
    address_line2: nil,
    address_state: nil,
    address_zip: nil,
    address_zip_check: nil,
    brand: "Visa",
    country: "US",
    customer: "cus_123",
    cvc_check: "unchecked",
    dynamic_last4: nil,
    exp_month: 11,
    exp_year: 2016,
    funding: "credit",
    last4: "4242",
    metadata: %{},
    name: nil,
    tokenization_method: nil
  }

  @local_map %{
    "id_from_stripe" => "card_123",
    "brand" => "Visa",
    "exp_month" => 11,
    "exp_year" => 2016,
    "last4" => "4242",
    "customer_id_from_stripe" => "cus_123",
    "cvc_check" => "unchecked",
    "name" => nil
  }

  describe "to_params/2" do
    test "converts from stripe map to local properly" do
      test_attributes = %{
        "user_id" => 123,
        "foo" => "bar"
      }
      expected_attributes = %{
        "user_id" => 123,
      }

      {:ok, result} = to_params(@stripe_platform_card, test_attributes)
      expected_map = Map.merge(@local_map, expected_attributes)

      assert result == expected_map
    end
  end
end


Résolution du premier exercice pour Elixir dans exercism

defmodule HelloWorld do
  @doc """
  Simply returns "Hello, World!"
  """
  @spec hello :: String.t()
  def hello do
    "Hello, World!"
  end
end

defmodule HelloWorld do
  @doc """
  Simply returns "Hello, World!"
  """
  @spec hello :: String.t()
  def hello do
    "Your implementation goes here"
  end
end


Add Serum.Build.FragmentGeneratorTest

Add Serum.Build.FragmentGeneratorTest


Uses Map.get to retrieve params

Uses Map.get to retrieve params

defmodule PhxBkmark.BookmarkController do
  use PhxBkmark.Web, :controller
  import Ecto.Query, only: [from: 2]
  @per_page 20
  plug :action

  def index(conn, params) do
    page = Map.get(params, "page", "1") |> String.to_integer

    bookmarks = bookmark_query |> paginate(page, @per_page) |> Repo.all |> Repo.preload [:user, :tags]
    render(conn, "index.html", bookmarks: bookmarks)
  end

  def new(conn, _params) do
    render conn, "new.html"
  end

  # We will use the params, so no _!
  def create(conn, params) do
    render conn, "index.html"
  end

  def bookmark_query do
    from b in PhxBkmark.Bookmark,
    order_by: [desc: b.updated_at],
    select: b
  end

  def paginate(query, page, per_page) do
    offset = (page - 1) * per_page
    from query,
    offset: ^offset,
    limit: ^per_page
  end
end

defmodule PhxBkmark.BookmarkController do
  use PhxBkmark.Web, :controller
  import Ecto.Query, only: [from: 2]
  @per_page 20
  plug :action

  def index(conn, %{"page" => page}) do
    if page do
      page = String.to_integer(page)
    else
      page = 1
    end

    bookmarks = bookmark_query |> paginate(page, @per_page) |> Repo.all |> Repo.preload [:user, :tags]
    render(conn, "index.html", bookmarks: bookmarks)
  end

  def new(conn, _params) do
    render conn, "new.html"
  end

  # We will use the params, so no _!
  def create(conn, params) do
    render conn, "index.html"
  end

  def bookmark_query do
    from b in PhxBkmark.Bookmark,
    order_by: [desc: b.updated_at],
    select: b
  end

  def paginate(query, page, per_page) do
    offset = (page - 1) * per_page
    from query,
    offset: ^offset,
    limit: ^per_page
  end
end


Adding grade to resource_review

Adding grade to resource_review

defmodule Survey.ResourceController do
  use Survey.Web, :controller

  require Logger
  alias Survey.Resource
  import Prelude
  alias Survey.Resource
  alias Survey.ResourceTag
  alias Survey.Repo

  plug :action

  def add(conn, params) do
    if params["f"] do
      Logger.info("Saving new resource")
      save_to_db(conn, params["f"])
      Survey.Grade.submit_grade(conn, "add_resource", 1.0)
    end

    already = Resource.user_submitted_no(conn.assigns.user.id)
    if already > 0 do
      conn = put_flash(conn, :info,
        "Thank you for submitting #{already} #{resource_word(already)}. Your participation has already been graded. You are welcome to submit more resources, or move on to other parts of the course.")
    end

    sig = conn.assigns.user.sig_id
    tags = ResourceTag.get_tags(sig)

    conn
    |> put_layout("minimal.html")
    |> render "resource.html", tags: tags
  end

  def resource_word(cnt) when cnt > 1, do: "resources"
  def resource_word(cnt), do: "resource"

  #----------------------------------------

  def tag_cloud(conn, params) do
    sig = conn.assigns.user.sig_id
    if params["all"], do: sig = nil
    tagfreq = Resource.tag_freq(sig)
    conn
    |> put_layout("minimal.html")
    |> render "tag_cloud.html", tagfreq: tagfreq
  end

  def list(conn, params) do
    sig = conn.assigns.user.sig_id
    if params["tag"] do
      tag = params["tag"]
      resources = Resource.resource_list(sig, params["tag"])
    else
      tag = nil
      resources = Resource.resource_list(sig)
    end

    conn
    |> put_layout("minimal.html")
    |> render "list.html", resources: resources, tag: tag
  end
  #----------------------------------------

  def review(conn, params) do
    if params["list"] do
      conn = put_session(conn, :review_redirect, true)
      redirect = :list
    else
      redirect = :next
      conn = delete_session(conn, :review_redirect)
    end
    user = conn.assigns.user
    already = Resource.user_reviewed_no(conn.assigns.user.id)
    if already > 0 do
      conn = put_flash(conn, :info,
        "Thank you for reviewing #{already} #{resource_word(already)}. You are welcome to review more resources, or move on to other parts of the course.")
      Survey.Grade.submit_grade(conn, "review_resource", 1.0)
    end

    if params["id"] do
      id = String.to_integer(params["id"])
      Resource.update_seen(user, id)
    else
      id = Resource.get_random(conn.assigns.user)
    end

    if !id do
      html conn, "Sorry, we could not find any new resources for you to review. Try back in a little while."
    else
      sig = conn.assigns.user.sig_id
      tags = ResourceTag.get_tags(sig)
      resource = Resource.get_resource(id)

      if !resource do
        ParamSession.redirect(conn, "/resource/review")
      else
      seen = Resource.user_seen?(user, resource.id)

        rtype = if resource.generic do
          "NOTE: This is a GENERIC Resource, meaning that the person who added it felt that it would be applicable to more than one SIG"
        else
          ""
        end

        conn
        |> put_layout("minimal.html")
        |> render "review.html", tags: tags, resource: resource,
          resourcetype: rtype, redirect: redirect, seen: seen
      end
    end
  end

  def review_submit(conn, params) do
    user = conn.assigns.user
    resource = Repo.get(Resource, params["resource_id"])
    form = params["f"]
    |> Enum.map(fn {k, v} -> {k, String.strip(v)} end)
    |> Enum.into(%{})

    # COMMENTS
    comments = resource.comments || []
    if string_param_exists(form["comment"]) do
      newcom = %{nick: user.nick, user_id: user.id, text: form["comment"],
        date: Ecto.DateTime.local}
      comments = [ newcom | comments ]
    end

    # DESCRIPTION
    description = resource.description
    old_desc = resource.old_desc
    if string_param_exists(form["description"]) &&
      String.strip(form["description"]) != String.strip(resource.description) do
      description = String.strip(form["description"])

      cur = %{description: form["description"], user_id: user.id,
        date: Ecto.DateTime.local}

      # either append, or create old_desc with orig as first entry
      if old_desc do
        old_desc = [ cur | old_desc ]
      else
        orig = %{description: resource.description,
          user_id: resource.user_id, date: resource.inserted_at}
        old_desc = [cur, orig]
      end
    end

    # SCORE
    score = resource.score || 0.0
    old_score = resource.old_score || []
    if string_param_exists(form["rating"]) do
      {newscore, _} = Float.parse(form["rating"])
      new_old_score = %{ "user" => user.id, "score" => newscore,
        "date" => Ecto.DateTime.local }
      old_score = [ new_old_score | old_score ]
      score_sum = old_score
      |> Enum.map(fn x -> x["score"] end)
      |> Enum.sum
      score = score_sum / length(old_score)
    end

    # TAGS
    tags = resource.tags
    if !(resource.generic || !form["tags"] || form["tags"] == "") do
      old_tags = resource.old_tags
      raw_tags = String.split(form["tags"], "|")
      new_tags = set_difference(raw_tags, tags)
      if !Enum.empty?(new_tags) do

        tags = raw_tags
        cur = %{tags: new_tags, user_id: user.id,
          date: Ecto.DateTime.local}

        # either append, or create old_desc with orig as first entry
        if old_tags do
          old_tags = [ cur | old_tags ]
        else
          orig = %{tags: resource.tags,
            user_id: resource.user_id, date: resource.inserted_at}
          old_tags = [cur, orig]
        end
      end
    end

    # INSERT DB
    %{ resource |
      tags: tags, old_tags: old_tags, score: score, old_score: old_score,
      description: description, old_desc: old_desc, comments: comments}
    |> Repo.update!

    redir_url = if get_session(conn, :review_redirect) do
      "/resource/list"
    else
      "/resource/review"
    end
    Survey.Grade.submit_grade(conn, "review_resource", 1.0)

    conn
    |> ParamSession.redirect redir_url
  end
  #----------------------------------------

  def set(x) when is_list(x) do
    Enum.into(x, HashSet.new)
  end

  def different_as_set?(x, y) when is_list(x) and is_list(y) do
    !Enum.empty?(set_difference(x, y))
  end

  def set_difference(x, y) when is_list(x) and is_list(y) do
    Set.difference(set(x), set(y))
    |> Enum.to_list
  end

  def preview(conn, params) do
    tags = ResourceTag.get_tags(2)
    conn
    |> put_layout("minimal.html")
    |> render "resource.html", tags: tags
  end

  def report(conn, _) do
    resources = Resource.get_all_by_sigs
    conn
    |> put_layout("minimal.html")
    |> render "report.html", resources: resources
  end
  #----------------------------------------

  def save_to_db(conn, params) do
    if !Resource.find_url(params["url"], conn.assigns.user.sig_id) do
      resource = params
      |> Map.update("generic", false, fn x -> x == "true" end)
      |> Map.put("user_id", conn.assigns.user.id)
      |> Map.put("sig_id", conn.assigns.user.sig_id)
      |> atomify_map
      |> proc_tags

      struct(Resource, resource)
      |> Repo.insert!

      ResourceTag.update_tags(conn.assigns.user.sig_id, resource.tags)
    else
      Logger.warn("Tried inserting resource with same URL twice")
    end
  end

  defp proc_tags(%{tags: tags} = h), do: %{h | tags: String.split(tags, "|") }
  defp proc_tags(x), do: x

  def check_url(conn, params) do
    sig = conn.assigns.user.sig_id
    url = params["url"] |> String.strip
    if id = Resource.find_url(url, sig) do
      json conn, %{result: "exists", id: id}
    else
      json conn, %{result: "success"}
    end
  end

  defp bad_status(s) do
    s |> Integer.to_string |> String.starts_with?("4")
  end

  defp string_param_exists(s) do
    s && String.strip(s) != ""
  end
end

defmodule Survey.ResourceController do
  use Survey.Web, :controller

  require Logger
  alias Survey.Resource
  import Prelude
  alias Survey.Resource
  alias Survey.ResourceTag
  alias Survey.Repo

  plug :action

  def add(conn, params) do
    if params["f"] do
      Logger.info("Saving new resource")
      save_to_db(conn, params["f"])
      Survey.Grade.submit_grade(conn, "add_resource", 1.0)
    end

    already = Resource.user_submitted_no(conn.assigns.user.id)
    if already > 0 do
      conn = put_flash(conn, :info,
        "Thank you for submitting #{already} #{resource_word(already)}. Your participation has already been graded. You are welcome to submit more resources, or move on to other parts of the course.")
    end

    sig = conn.assigns.user.sig_id
    tags = ResourceTag.get_tags(sig)

    conn
    |> put_layout("minimal.html")
    |> render "resource.html", tags: tags
  end

  def resource_word(cnt) when cnt > 1, do: "resources"
  def resource_word(cnt), do: "resource"

  #----------------------------------------

  def tag_cloud(conn, params) do
    sig = conn.assigns.user.sig_id
    if params["all"], do: sig = nil
    tagfreq = Resource.tag_freq(sig)
    conn
    |> put_layout("minimal.html")
    |> render "tag_cloud.html", tagfreq: tagfreq
  end

  def list(conn, params) do
    sig = conn.assigns.user.sig_id
    if params["tag"] do
      tag = params["tag"]
      resources = Resource.resource_list(sig, params["tag"])
    else
      tag = nil
      resources = Resource.resource_list(sig)
    end

    conn
    |> put_layout("minimal.html")
    |> render "list.html", resources: resources, tag: tag
  end
  #----------------------------------------

  def review(conn, params) do
    if params["list"] do
      conn = put_session(conn, :review_redirect, true)
      redirect = :list
    else
      redirect = :next
      conn = delete_session(conn, :review_redirect)
    end
    user = conn.assigns.user
    already = Resource.user_reviewed_no(conn.assigns.user.id)
    if already > 0 do
      conn = put_flash(conn, :info,
        "Thank you for reviewing #{already} #{resource_word(already)}. You are welcome to review more resources, or move on to other parts of the course.")
    end

    if params["id"] do
      id = String.to_integer(params["id"])
      Resource.update_seen(user, id)
    else
      id = Resource.get_random(conn.assigns.user)
    end

    if !id do
      html conn, "Sorry, we could not find any new resources for you to review. Try back in a little while."
    else
      sig = conn.assigns.user.sig_id
      tags = ResourceTag.get_tags(sig)
      resource = Resource.get_resource(id)

      if !resource do
        ParamSession.redirect(conn, "/resource/review")
      else
      seen = Resource.user_seen?(user, resource.id)

        rtype = if resource.generic do
          "NOTE: This is a GENERIC Resource, meaning that the person who added it felt that it would be applicable to more than one SIG"
        else
          ""
        end

        conn
        |> put_layout("minimal.html")
        |> render "review.html", tags: tags, resource: resource,
          resourcetype: rtype, redirect: redirect, seen: seen
      end
    end
  end

  def review_submit(conn, params) do
    user = conn.assigns.user
    resource = Repo.get(Resource, params["resource_id"])
    form = params["f"]
    |> Enum.map(fn {k, v} -> {k, String.strip(v)} end)
    |> Enum.into(%{})

    # COMMENTS
    comments = resource.comments || []
    if string_param_exists(form["comment"]) do
      newcom = %{nick: user.nick, user_id: user.id, text: form["comment"],
        date: Ecto.DateTime.local}
      comments = [ newcom | comments ]
    end

    # DESCRIPTION
    description = resource.description
    old_desc = resource.old_desc
    if string_param_exists(form["description"]) &&
      String.strip(form["description"]) != String.strip(resource.description) do
      description = String.strip(form["description"])

      cur = %{description: form["description"], user_id: user.id,
        date: Ecto.DateTime.local}

      # either append, or create old_desc with orig as first entry
      if old_desc do
        old_desc = [ cur | old_desc ]
      else
        orig = %{description: resource.description,
          user_id: resource.user_id, date: resource.inserted_at}
        old_desc = [cur, orig]
      end
    end

    # SCORE
    score = resource.score || 0.0
    old_score = resource.old_score || []
    if string_param_exists(form["rating"]) do
      {newscore, _} = Float.parse(form["rating"])
      new_old_score = %{ "user" => user.id, "score" => newscore,
        "date" => Ecto.DateTime.local }
      old_score = [ new_old_score | old_score ]
      score_sum = old_score
      |> Enum.map(fn x -> x["score"] end)
      |> Enum.sum
      score = score_sum / length(old_score)
    end

    # TAGS
    tags = resource.tags
    if !(resource.generic || !form["tags"] || form["tags"] == "") do
      old_tags = resource.old_tags
      raw_tags = String.split(form["tags"], "|")
      new_tags = set_difference(raw_tags, tags)
      if !Enum.empty?(new_tags) do

        tags = raw_tags
        cur = %{tags: new_tags, user_id: user.id,
          date: Ecto.DateTime.local}

        # either append, or create old_desc with orig as first entry
        if old_tags do
          old_tags = [ cur | old_tags ]
        else
          orig = %{tags: resource.tags,
            user_id: resource.user_id, date: resource.inserted_at}
          old_tags = [cur, orig]
        end
      end
    end

    # INSERT DB
    %{ resource |
      tags: tags, old_tags: old_tags, score: score, old_score: old_score,
      description: description, old_desc: old_desc, comments: comments}
    |> Repo.update!

    redir_url = if get_session(conn, :review_redirect) do
      "/resource/list"
    else
      "/resource/review"
    end

    conn
    |> ParamSession.redirect redir_url
  end
  #----------------------------------------

  def set(x) when is_list(x) do
    Enum.into(x, HashSet.new)
  end

  def different_as_set?(x, y) when is_list(x) and is_list(y) do
    !Enum.empty?(set_difference(x, y))
  end

  def set_difference(x, y) when is_list(x) and is_list(y) do
    Set.difference(set(x), set(y))
    |> Enum.to_list
  end

  def preview(conn, params) do
    tags = ResourceTag.get_tags(2)
    conn
    |> put_layout("minimal.html")
    |> render "resource.html", tags: tags
  end

  def report(conn, _) do
    resources = Resource.get_all_by_sigs
    conn
    |> put_layout("minimal.html")
    |> render "report.html", resources: resources
  end
  #----------------------------------------

  def save_to_db(conn, params) do
    if !Resource.find_url(params["url"], conn.assigns.user.sig_id) do
      resource = params
      |> Map.update("generic", false, fn x -> x == "true" end)
      |> Map.put("user_id", conn.assigns.user.id)
      |> Map.put("sig_id", conn.assigns.user.sig_id)
      |> atomify_map
      |> proc_tags

      struct(Resource, resource)
      |> Repo.insert!

      ResourceTag.update_tags(conn.assigns.user.sig_id, resource.tags)
    else
      Logger.warn("Tried inserting resource with same URL twice")
    end
  end

  defp proc_tags(%{tags: tags} = h), do: %{h | tags: String.split(tags, "|") }
  defp proc_tags(x), do: x

  def check_url(conn, params) do
    sig = conn.assigns.user.sig_id
    url = params["url"] |> String.strip
    if id = Resource.find_url(url, sig) do
      json conn, %{result: "exists", id: id}
    else
      json conn, %{result: "success"}
    end
  end

  defp bad_status(s) do
    s |> Integer.to_string |> String.starts_with?("4")
  end

  defp string_param_exists(s) do
    s && String.strip(s) != ""
  end
end


Add inline adapter configuration documentation

defmodule Geolix.Adapter.MMDB2 do
  @moduledoc """
  Adapter for Geolix to work with MMDB2 databases.

  ## Adapter Configuration

  To start using the adapter with a compatible database you need to add the
  required configuration entry to your `:geolix` configuration:

      config :geolix,
        databases: [
          %{
            id: :my_mmdb_database,
            adapter: Geolix.Adapter.MMDB2,
            source: "/absolute/path/to/my/database.mmdb"
          }
        ]
  """

  alias Geolix.Adapter.MMDB2.Database
  alias Geolix.Adapter.MMDB2.Loader
  alias Geolix.Adapter.MMDB2.Storage

  @behaviour Geolix.Adapter

  @impl Geolix.Adapter
  def database_workers do
    import Supervisor.Spec

    [
      worker(Storage.Data, []),
      worker(Storage.Metadata, []),
      worker(Storage.Tree, [])
    ]
  end

  @impl Geolix.Adapter
  def load_database(database), do: Loader.load_database(database)

  @impl Geolix.Adapter
  def lookup(ip, opts), do: Database.lookup(ip, opts)

  @impl Geolix.Adapter
  def unload_database(database), do: Loader.unload_database(database)
end

defmodule Geolix.Adapter.MMDB2 do
  @moduledoc """
  Adapter for Geolix to work with MMDB2 databases.
  """

  alias Geolix.Adapter.MMDB2.Database
  alias Geolix.Adapter.MMDB2.Loader
  alias Geolix.Adapter.MMDB2.Storage

  @behaviour Geolix.Adapter

  @impl Geolix.Adapter
  def database_workers do
    import Supervisor.Spec

    [
      worker(Storage.Data, []),
      worker(Storage.Metadata, []),
      worker(Storage.Tree, [])
    ]
  end

  @impl Geolix.Adapter
  def load_database(database), do: Loader.load_database(database)

  @impl Geolix.Adapter
  def lookup(ip, opts), do: Database.lookup(ip, opts)

  @impl Geolix.Adapter
  def unload_database(database), do: Loader.unload_database(database)
end


better naming

better naming

defmodule Utils do
  use Timex

  def time_as_string do
    {hh, mm, ss} = :erlang.time()
    :io_lib.format("~2.10.0B:~2.10.0B:~2.10.0B", [hh, mm, ss])
    |> :erlang.list_to_binary()
  end

  def datetime_as_string do
  	{:ok, dt_string} = Timex.format(Timex.now, "{ISO:Extended}")
  	dt_string
  end

  def datetime_as_string(shift_minutes) do
    {:ok, dt_string} = Timex.format(Timex.shift(Timex.now, minutes: shift_minutes), "{ISO:Extended}")
  	dt_string
  end

  def default(val, default) do
    case val do
      nil -> default
      _ -> val
    end
  end

end

defmodule Utils do
  use Timex

  def time_as_string do
    {hh, mm, ss} = :erlang.time()
    :io_lib.format("~2.10.0B:~2.10.0B:~2.10.0B", [hh, mm, ss])
    |> :erlang.list_to_binary()
  end

  def datetime_as_string do
  	{:ok, default_str} = Timex.format(Timex.now, "{ISO:Extended}")
  	default_str
  end

  def datetime_as_string(shift_minutes) do
    {:ok, default_str} = Timex.format(Timex.shift(Timex.now, minutes: shift_minutes), "{ISO:Extended}")
  	default_str
  end

  def default(val, default) do
    case val do
      nil -> default
      _ -> val
    end
  end

end


Avoid reading files multiple times when expanding macros

Code.require_file "../helpers/introspection.exs", __DIR__
Code.require_file "../code/metadata.exs", __DIR__
Code.require_file "../code/parser.exs", __DIR__
Code.require_file "../code/ast.exs", __DIR__

defmodule Alchemist.API.Eval do

  @moduledoc false

  alias Alchemist.Code.Metadata
  alias Alchemist.Code.Parser
  alias Alchemist.Code.Ast

  def request(args) do
    args
    |> normalize
    |> process

    IO.puts "END-OF-EVAL"
  end

  def process({:eval, file}) do
    try do
      File.read!("#{file}")
      |> Code.eval_string
      |> Tuple.to_list
      |> List.first
      |> IO.inspect
    rescue
      e -> IO.inspect e
    end
  end

  def process({:match, file}) do
    try do
      file_content = File.read!("#{file}")
      {:=, _, [pattern|_]} = file_content |> Code.string_to_quoted!
      vars = extract_vars(pattern)

      bindings = file_content
      |> Code.eval_string
      |> Tuple.to_list
      |> List.last

      if Enum.empty?(vars) do
        IO.puts "# No bindings"
      else
        IO.puts "# Bindings"
      end

      Enum.each(vars, fn var ->
        IO.puts ""
        IO.write "#{var} = "
        IO.inspect Keyword.get(bindings, var)
      end)
    rescue
      e -> print_match_error(e)
    end
  end

  def process({:quote, file}) do
    try do
      File.read!("#{file}")
      |> Code.string_to_quoted
      |> Tuple.to_list
      |> List.last
      |> IO.inspect
    rescue
      e -> IO.inspect e
    end
  end

  def process({:expand_once, buffer_file, file, line}) do
    try do
      {_, expr} = File.read!("#{file}") |> Code.string_to_quoted
      env = create_env(buffer_file, line)
      expand_and_print(&Macro.expand_once/2, expr, env)
    rescue
      e -> IO.inspect e
    end
  end

  def process({:expand, buffer_file, file, line}) do
    try do
      {_, expr} = File.read!("#{file}") |> Code.string_to_quoted
      env = create_env(buffer_file, line)
      expand_and_print(&Macro.expand/2, expr, env)
    rescue
      e -> IO.inspect e
    end
  end

  def process({:expand_partial, buffer_file, file, line}) do
    try do
      {_, expr} = File.read!("#{file}")
      |> Code.string_to_quoted
      env = create_env(buffer_file, line)
      expand_and_print(&Ast.expand_partial/2, expr, env)
    rescue
      e -> IO.inspect e
    end
  end

  def process({:expand_all, buffer_file, file, line}) do
    try do
      {_, expr} = File.read!("#{file}") |> Code.string_to_quoted
      env = create_env(buffer_file, line)
      expand_and_print(&Ast.expand_all/2, expr, env)
    rescue
      e -> IO.inspect e
    end
  end

  def process({:expand_full, buffer_file, file, line}) do
    try do
      {_, expr} = File.read!("#{file}") |> Code.string_to_quoted
      env = create_env(buffer_file, line)
      expand_and_print(&Macro.expand_once/2, expr, env)
      IO.puts("\u000B")
      expand_and_print(&Macro.expand/2, expr, env)
      IO.puts("\u000B")
      expand_and_print(&Ast.expand_partial/2, expr, env)
      IO.puts("\u000B")
      expand_and_print(&Ast.expand_all/2, expr, env)
    rescue
      e -> IO.inspect e
    end
  end

  defp expand_and_print(expand_func, expr, env) do
    expand_func.(expr, env)
    |> Macro.to_string
    |> IO.puts
  end

  def normalize(request) do
    {expr , _} = Code.eval_string(request)
    expr
  end

  defp extract_vars(ast) do
    {_ast, acc} = Macro.postwalk(ast, [], &extract_var/2)
    acc |> Enum.reverse
  end

  defp extract_var(ast = {var_name, [line: _], nil}, acc) when is_atom(var_name) and var_name != :_ do
    {ast, [var_name|acc]}
  end

  defp extract_var(ast, acc) do
    {ast, acc}
  end

  defp print_match_error(%{__struct__: type, description: description, line: line}) do
    IO.puts "# #{Introspection.module_to_string(type)} on line #{line}: \n#  ↳ #{description}"
  end

  defp print_match_error(%MatchError{}) do
    IO.puts "# No match"
  end

  defp print_match_error(e) do
    IO.inspect(e)
  end

  defp create_env(file, line) do
    %{requires: requires, imports: imports, module: module} =
      file
      |> Parser.parse_file(true, true, line)
      |> Metadata.get_env(line)

    __ENV__
    |> Ast.add_requires_to_env(requires)
    |> Ast.add_imports_to_env(imports)
    |> Ast.set_module_for_env(module)
  end

end

Code.require_file "../helpers/introspection.exs", __DIR__
Code.require_file "../code/metadata.exs", __DIR__
Code.require_file "../code/parser.exs", __DIR__
Code.require_file "../code/ast.exs", __DIR__

defmodule Alchemist.API.Eval do

  @moduledoc false

  alias Alchemist.Code.Metadata
  alias Alchemist.Code.Parser
  alias Alchemist.Code.Ast

  def request(args) do
    args
    |> normalize
    |> process

    IO.puts "END-OF-EVAL"
  end

  def process({:eval, file}) do
    try do
      File.read!("#{file}")
      |> Code.eval_string
      |> Tuple.to_list
      |> List.first
      |> IO.inspect
    rescue
      e -> IO.inspect e
    end
  end

  def process({:match, file}) do
    try do
      file_content = File.read!("#{file}")
      {:=, _, [pattern|_]} = file_content |> Code.string_to_quoted!
      vars = extract_vars(pattern)

      bindings = file_content
      |> Code.eval_string
      |> Tuple.to_list
      |> List.last

      if Enum.empty?(vars) do
        IO.puts "# No bindings"
      else
        IO.puts "# Bindings"
      end

      Enum.each(vars, fn var ->
        IO.puts ""
        IO.write "#{var} = "
        IO.inspect Keyword.get(bindings, var)
      end)
    rescue
      e -> print_match_error(e)
    end
  end

  def process({:quote, file}) do
    try do
      File.read!("#{file}")
      |> Code.string_to_quoted
      |> Tuple.to_list
      |> List.last
      |> IO.inspect
    rescue
      e -> IO.inspect e
    end
  end

  def process({:expand, buffer_file, file, line}) do
    try do
      {_, expr} = File.read!("#{file}")
      |> Code.string_to_quoted
      env = create_env(buffer_file, line)
      res = Macro.expand(expr, env)
      IO.puts Macro.to_string(res)
    rescue
      e -> IO.inspect e
    end
  end

  def process({:expand_once, buffer_file, file, line}) do
    try do
      {_, expr} = File.read!("#{file}")
      |> Code.string_to_quoted
      env = create_env(buffer_file, line)
      res = Macro.expand_once(expr, env)
      IO.puts Macro.to_string(res)
    rescue
      e -> IO.inspect e
    end
  end

  def process({:expand_partial, buffer_file, file, line}) do
    try do
      {_, expr} = File.read!("#{file}")
      |> Code.string_to_quoted
      env = create_env(buffer_file, line)
      res = Ast.expand_partial(expr, env)
      IO.puts Macro.to_string(res)
    rescue
      e -> IO.inspect e
    end
  end

  def process({:expand_all, buffer_file, file, line}) do
    try do
      {_, expr} = File.read!("#{file}")
      |> Code.string_to_quoted
      env = create_env(buffer_file, line)
      res = Ast.expand_all(expr, env)
      IO.puts Macro.to_string(res)
    rescue
      e -> IO.inspect e
    end
  end

  def process({:expand_full, buffer_file, file, line}) do
    process({:expand_once, buffer_file, file, line})
    IO.puts("\u000B")
    process({:expand, buffer_file, file, line})
    IO.puts("\u000B")
    process({:expand_partial, buffer_file, file, line})
    IO.puts("\u000B")
    process({:expand_all, buffer_file, file, line})
  end

  def normalize(request) do
    {expr , _} = Code.eval_string(request)
    expr
  end

  defp extract_vars(ast) do
    {_ast, acc} = Macro.postwalk(ast, [], &extract_var/2)
    acc |> Enum.reverse
  end

  defp extract_var(ast = {var_name, [line: _], nil}, acc) when is_atom(var_name) and var_name != :_ do
    {ast, [var_name|acc]}
  end

  defp extract_var(ast, acc) do
    {ast, acc}
  end

  defp print_match_error(%{__struct__: type, description: description, line: line}) do
    IO.puts "# #{Introspection.module_to_string(type)} on line #{line}: \n#  ↳ #{description}"
  end

  defp print_match_error(%MatchError{}) do
    IO.puts "# No match"
  end

  defp print_match_error(e) do
    IO.inspect(e)
  end

  defp create_env(file, line) do
    %{requires: requires, imports: imports, module: module} =
      file
      |> Parser.parse_file(true, true, line)
      |> Metadata.get_env(line)

    __ENV__
    |> Ast.add_requires_to_env(requires)
    |> Ast.add_imports_to_env(imports)
    |> Ast.set_module_for_env(module)
  end

end


fix broken sync

fix broken sync

defmodule Yodado.FeaturePersistence.Mnesia do

  @table_record Yodado.Feature.Feature

  @all_features_matcher Yodado.Feature.Feature.__record__(:fields) |>
    Enum.map(fn({key, _default}) -> {key, :_} end) |>
    Yodado.Feature.Feature.new

  def all() do
    features = :mnesia.dirty_match_object(@all_features_matcher)
    {:ok, features}
  end

  def find_feature({:key, feature_id}) do
    features = :mnesia.dirty_read(@table_record, feature_id)
    case features do
      [feature] ->
        {:ok, feature}
      [] ->
        :not_found
    end
  end

  def find_feature(feature_id) do
    find_feature({:key, key(feature_id)})
  end

  def save_feature(feature) do
    :ok = :mnesia.transaction(fn() ->
      :mnesia.write(feature)
    end)
    {:ok, true}
  end

  def delete_feature({:key, feature_id}) do
    :ok = :mnesia.transaction(fn() ->
      :mnesia.delete({@table_record, feature_id})
    end)
    {:ok, true}
  end

  def delete_feature(feature_id) do
    delete_feature({:key, feature_id})
  end

  #TODO this must die. only used by old JS client
  def sync(features) do
    {:atomic, :ok} =  :mnesia.transaction(fn() ->
      #TODO slow, and hacky...
      keys = :mnesia.all_keys(@table_record)
      Enum.each(keys, fn(k) ->
        :ok = :mnesia.delete({@table_record, k})
      end)

      Enum.each(features, fn(f) ->
        :ok = :mnesia.write(f)
      end)
    end)
    {:ok, true}
  end

  defp key(feature) when is_record(feature, @table_record) do
    feature.title
  end

  defp key(feature_id) do
    feature_id
  end
end

defmodule Yodado.FeaturePersistence.Mnesia do

  @table_record Yodado.Feature.Feature

  @all_features_matcher Yodado.Feature.Feature.__record__(:fields) |>
    Enum.map(fn({key, _default}) -> {key, :_} end) |>
    Yodado.Feature.Feature.new

  def all() do
    features = :mnesia.dirty_match_object(@all_features_matcher)
    {:ok, features}
  end

  def find_feature({:key, feature_id}) do
    features = :mnesia.dirty_read(@table_record, feature_id)
    case features do
      [feature] ->
        {:ok, feature}
      [] ->
        :not_found
    end
  end

  def find_feature(feature_id) do
    find_feature({:key, key(feature_id)})
  end

  def save_feature(feature) do
    :ok = :mnesia.transaction(fn() ->
      :mnesia.write(feature)
    end)
    {:ok, true}
  end

  def delete_feature({:key, feature_id}) do
    :ok = :mnesia.transaction(fn() ->
      :mnesia.delete({@table_record, feature_id})
    end)
    {:ok, true}
  end

  def delete_feature(feature_id) do
    delete_feature({:key, feature_id})
  end

  #TODO this must die
  def sync(features) do
    # TODO not transactional. tut, tut, tut
    {:atomic, :ok} = :mnesia.clear_table(@table_record)
    :mnesia.transaction(fn() ->
      Enum.each(features, fn(f) ->
        {:atomic, :ok} = :mnesia.write(f)
      end)
    end)
    {:ok, true}
  end

  defp key(feature) when is_record(feature, @table_record) do
    feature.title
  end

  defp key(feature_id) do
    feature_id
  end
end


Refactored monitor so there can be multiple running supervised

defmodule PhoenixTrello.BoardChannel.Monitor do
  @moduledoc """
  Board monitor that keeps track of connected users.
  """

  use GenServer

  #####
  # External API

  def create(board_id) do
    case GenServer.whereis(ref(board_id)) do
      nil ->
        Supervisor.start_child(PhoenixTrello.BoardChannel.Supervisor, [board_id])
      _board ->
        {:error, :board_already_exists}
    end
  end

  def start_link(board_id) do
    GenServer.start_link(__MODULE__, [], name: ref(board_id))
  end

  def user_joined(board_id, user) do
   try_call board_id, {:user_joined, user}
  end

  def users_in_board_id(board_id) do
   try_call board_id, {:users_in_board_id}
  end

  def user_left(board_id, user) do
    try_call board_id, {:user_left, user}
  end

  #####
  # GenServer implementation

  def handle_call({:user_joined, user}, _from, users) do
    users = [user] ++ users
      |> Enum.uniq

    {:reply, users, users}
  end

  def handle_call({:users_in_board_id}, _from, users) do
    { :reply, users, users }
  end

  def handle_call({:user_left, user}, _from, users) do
    users = List.delete(users, user)
    {:reply, users, users}
  end

  defp ref(board_id) do
    {:global, {:board, board_id}}
  end

  defp try_call(board_id, call_function) do
    case GenServer.whereis(ref(board_id)) do
      nil ->
        {:error, :invalid_board}
      board ->
        GenServer.call(board, call_function)
    end
  end
end

defmodule PhoenixTrello.BoardChannel.Monitor do
  @moduledoc """
  Board channel monitor that keeps track of connected users per board.
  """

  use GenServer

  #####
  # External API

  def start_link(initial_state) do
   GenServer.start_link(__MODULE__, initial_state, name: __MODULE__)
  end

  def user_joined(channel, user) do
   GenServer.call(__MODULE__, {:user_joined, channel, user})
  end

  def users_in_channel(channel) do
   GenServer.call(__MODULE__, {:users_in_channel, channel})
  end

  def user_left(channel, user_id) do
    GenServer.call(__MODULE__, {:user_left, channel, user_id})
  end

  #####
  # GenServer implementation

  def handle_call({:user_joined, channel, user}, _from, state) do
    state = case Map.get(state, channel) do
      nil ->
        state |> Map.put(channel, [user])
      users ->
        state |> Map.put(channel, Enum.uniq([user | users]))
    end

    {:reply, state, state}
  end

  def handle_call({:users_in_channel, channel}, _from, state) do
    { :reply,  Map.get(state, channel), state }
  end

  def handle_call({:user_left, channel, user_id}, _from, state) do
    new_users = state
      |> Map.get(channel)
      |> Enum.reject(&(&1.id == user_id))

    state = state
      |> Map.update!(channel, fn(_) -> new_users end)

    {:reply, state, state}
  end
end