Change test example in lib/bamboo/test.ex from MyApp.EmailsTest to MyApp.EmailTest

defmodule Bamboo.Test do
  import ExUnit.Assertions

  @moduledoc """
  Helpers for testing email delivery

  Use these helpers with Bamboo.TestAdapter to test email delivery. Typically
  you'll want to **unit test emails first**. Then in integration tests use
  helpers from this module to test whether that email was delivered.

  ## Note on sending from other processes

  If you are sending emails from another process (for example, from inside a
  Task or GenServer) you may need to use shared mode when using
  `Bamboo.Test`. See the docs `__using__/1` for an example.

  For most scenarios you will not need shared mode.

  ## In your config

      # Typically in config/test.exs
      config :my_app, MyApp.Mailer,
        adapter: Bamboo.TestAdapter

  ## Unit test

  You don't need any special functions to unit test emails.

      defmodule MyApp.EmailTest do
        use ExUnit.Case

        test "welcome email" do
          user = %User{name: "John", email: "person@example.com"}

          email = MyApp.Email.welcome_email(user)

          assert email.to == user
          assert email.subject == "This is your welcome email"
          assert email.html_body =~ "Welcome to the app"
        end
      end

  ## Integration test

      defmodule MyApp.Email do
        import Bamboo.Email

        def welcome_email(user) do
          new_email(
            from: "me@app.com",
            to: user,
            subject: "Welcome!",
            text_body: "Welcome to the app",
            html_body: "Welcome to the app"
          )
        end
      end

      defmodule MyApp.EmailDeliveryTest do
        use ExUnit.Case
        use Bamboo.Test

        test "sends welcome email" do
          user = %User{...}
          email = MyApp.Email.welcome_email(user)

          email |> MyApp.Mailer.deliver_now

          # Works with deliver_now and deliver_later
          assert_delivered_email email
        end
      end
  """

  @doc """
  Imports Bamboo.Test and Bamboo.Formatter.format_email_address/2

  `Bamboo.Test` and the `Bamboo.TestAdapter` work by sending a message to the
  current process when an email is delivered. The process mailbox is then
  checked when using the assertion helpers like `assert_delivered_email/1`.

  Sometimes emails don't show up when asserting because you may deliver an email
  from a _different_ process than the test process. When that happens, turn on
  shared mode. This will tell `Bamboo.TestAdapter` to always send to the test process.
  This means that you cannot use shared mode with async tests.

  ## Try to use this version first

      use Bamboo.Test

  ## And if you are delivering from another process, set `shared: true`

      use Bamboo.Test, shared: true

  Common scenarios for delivering mail from a different process are when you
  send from inside of a Task, GenServer, or are running acceptance tests with a
  headless browser like phantomjs.
  """
  defmacro __using__(shared: true) do
    quote do
      setup tags do
        if tags[:async] do
          raise """
          you cannot use Bamboo.Test shared mode with async tests.

          There are a few options, the 1st is the easiest:

            1) Set your test to [async: false].
            2) If you are delivering emails from another process (for example,
               delivering from within Task.async or Process.spawn), try using
               Mailer.deliver_later. If you use Mailer.deliver_later without
               spawning another process you can use Bamboo.Test with [async:
               true] and without the shared mode.
            3) If you are writing an acceptance test that requires shared mode,
               try using a controller test instead. Then see if the test works
               without shared mode.
          """
        else
          Application.put_env(:bamboo, :shared_test_process, self())
        end

        :ok
      end

      import Bamboo.Formatter, only: [format_email_address: 2]
      import Bamboo.Test
    end
  end

  defmacro __using__(_opts) do
    quote do
      setup tags do
        Application.delete_env(:bamboo, :shared_test_process)

        :ok
      end

      import Bamboo.Formatter, only: [format_email_address: 2]
      import Bamboo.Test
    end
  end

  @doc """
  Checks whether an email was delivered.

  Must be used with the `Bamboo.TestAdapter` or this will never pass. In case you
  are delivering from another process, the assertion waits up to 100ms before
  failing. Typically if an email is successfully delivered the assertion will
  pass instantly, so test suites will remain fast.

  ## Examples

      email = Bamboo.Email.new_email(subject: "something")
      email |> MyApp.Mailer.deliver
      assert_delivered_email(email) # Will pass

      unsent_email = Bamboo.Email.new_email(subject: "something else")
      assert_delivered_email(unsent_email) # Will fail
  """
  defmacro assert_delivered_email(email) do
    quote do
      import ExUnit.Assertions
      email = Bamboo.Test.normalize_for_testing(unquote(email))
      assert_receive({:delivered_email, ^email}, 100, Bamboo.Test.flunk_with_email_list(email))
    end
  end

  @doc """
  Check whether an email's params are equal to the ones provided.

  Must be used with the `Bamboo.TestAdapter` or this will never pass. In case you
  are delivering from another process, the assertion waits up to 100ms before
  failing. Typically if an email is successfully delivered the assertion will
  pass instantly, so test suites will remain fast.

  ## Examples

      email = Bamboo.Email.new_email(subject: "something")
      email |> MyApp.Mailer.deliver
      assert_email_delivered_with(subject: "something") # Will pass

      unsent_email = Bamboo.Email.new_email(subject: "something else")
      assert_email_delivered_with(subject: "something else") # Will fail

  You can also pass a regex to match portions of an email.

  ## Example

      email = new_email(text_body: "I love coffee")
      assert_email_delivered_with(email, text_body: ~r/love/) # Will pass
      assert_email_delivered_with(email, text_body: ~r/like/) # Will fail
  """
  defmacro assert_email_delivered_with(email_params) do
    quote bind_quoted: [email_params: email_params] do
      import ExUnit.Assertions
      assert_receive({:delivered_email, email}, 100, Bamboo.Test.flunk_no_emails_received)

      recieved_email_params = email |> Map.from_struct
      assert Enum.all?(email_params, fn({k, v}) -> do_match(recieved_email_params[k], v) end),
        Bamboo.Test.flunk_attributes_do_not_match(email_params, recieved_email_params)
    end
  end

  @doc false
  def do_match(value1, value2 = %Regex{}) do
    Regex.match?(value2, value1)
  end

  @doc false
  def do_match(value1, value2) do
    value1 == value2
  end

  @doc false
  def flunk_with_email_list(email) do
    if Enum.empty?(delivered_emails()) do
      flunk_no_emails_received()
    else
      flunk """
      There were no matching emails.

      No emails matched:

        #{inspect email}

      Delivered emails:

      #{delivered_emails_as_list()}
      """
    end
  end

  @doc false
  def flunk_no_emails_received do
    flunk """
    There were 0 emails delivered to this process.

    If you expected an email to be sent, try these ideas:

    1) Make sure you call deliver_now/1 or deliver_later/1 to deliver the email
    2) Make sure you are using the Bamboo.TestAdapter
    3) Use shared mode with Bamboo.Test. This will allow Bamboo.Test
    to work across processes: use Bamboo.Test, shared: :true
    4) If you are writing an acceptance test through a headless browser, use
    shared mode as described in option 3.
    """
  end

  @doc false
  def flunk_attributes_do_not_match(params_given, params_received) do
    """
    The parameters given do not match.

      Parameters given:

        #{inspect params_given}

      Email recieved:

        #{inspect params_received}
    """
  end

  defp delivered_emails do
    {:messages, messages} = Process.info(self(), :messages)

    for {:delivered_email, _} = email_message <- messages do
      email_message
    end
  end

  defp delivered_emails_as_list do
    delivered_emails() |> add_asterisk |> Enum.join("\n")
  end

  defp add_asterisk(emails) do
    Enum.map(emails, &" * #{inspect &1}")
  end

  @doc """
  Checks that no emails were sent.

  If `Bamboo.Test` is used with shared mode, you must also configure a timeout
  in your test config.

      # Set this in your config, typically in config/test.exs
      config :bamboo, :refute_timeout, 10

  The value you set is up to you. Lower values will result in faster tests,
  but may incorrectly pass if an email is delivered *after* the timeout. Often
  times 1ms is enough.
  """
  def assert_no_emails_delivered do
    receive do
      {:delivered_email, email} -> flunk_with_unexpected_email(email)
    after
      refute_timeout() -> true
    end
  end

  @doc false
  def assert_no_emails_sent do
    raise "assert_no_emails_sent/0 has been renamed to assert_no_emails_delivered/0"
  end

  defp flunk_with_unexpected_email(email) do
    flunk """
    Unexpectedly delivered an email when expected none to be delivered.

    Delivered email:

      #{inspect email}
    """
  end

  @doc """
  Ensures a particular email was not sent

  Same as `assert_delivered_email/0`, except it checks that a particular email
  was not sent.

  If `Bamboo.Test` is used with shared mode, you must also configure a timeout
  in your test config.

      # Set this in your config, typically in config/test.exs
      config :bamboo, :refute_timeout, 10

  The value you set is up to you. Lower values will result in faster tests,
  but may incorrectly pass if an email is delivered *after* the timeout. Often
  times 1ms is enough.
  """
  def refute_delivered_email(%Bamboo.Email{} = email) do
    email = normalize_for_testing(email)

    receive do
      {:delivered_email, ^email} -> flunk_with_unexpected_matching_email(email)
    after
      refute_timeout() -> true
    end
  end

  defp flunk_with_unexpected_matching_email(email) do
    flunk """
    Unexpectedly delivered a matching email.

    Matched email that was delivered:

      #{inspect email}
    """
  end

  defp refute_timeout do
    if using_shared_mode?() do
      Application.get_env(:bamboo, :refute_timeout) || raise """
      When using shared mode with Bamboo.Test, you must set a timeout. This
      is because an email can be delivered after the assertion is called.

          # Set this in your config, typically in config/test.exs
          config :bamboo, :refute_timeout, 10

      The value you set is up to you. Lower values will result in faster tests,
      but may incorrectly pass if an email is delivered *after* the timeout.
      """
    else
      0
    end
  end

  defp using_shared_mode? do
    !!Application.get_env(:bamboo, :shared_test_process)
  end

  @doc false
  def normalize_for_testing(email) do
    email
    |> Bamboo.Mailer.normalize_addresses
    |> Bamboo.TestAdapter.clean_assigns
  end
end

defmodule Bamboo.Test do
  import ExUnit.Assertions

  @moduledoc """
  Helpers for testing email delivery

  Use these helpers with Bamboo.TestAdapter to test email delivery. Typically
  you'll want to **unit test emails first**. Then in integration tests use
  helpers from this module to test whether that email was delivered.

  ## Note on sending from other processes

  If you are sending emails from another process (for example, from inside a
  Task or GenServer) you may need to use shared mode when using
  `Bamboo.Test`. See the docs `__using__/1` for an example.

  For most scenarios you will not need shared mode.

  ## In your config

      # Typically in config/test.exs
      config :my_app, MyApp.Mailer,
        adapter: Bamboo.TestAdapter

  ## Unit test

  You don't need any special functions to unit test emails.

      defmodule MyApp.EmailsTest do
        use ExUnit.Case

        test "welcome email" do
          user = %User{name: "John", email: "person@example.com"}

          email = MyApp.Email.welcome_email(user)

          assert email.to == user
          assert email.subject == "This is your welcome email"
          assert email.html_body =~ "Welcome to the app"
        end
      end

  ## Integration test

      defmodule MyApp.Email do
        import Bamboo.Email

        def welcome_email(user) do
          new_email(
            from: "me@app.com",
            to: user,
            subject: "Welcome!",
            text_body: "Welcome to the app",
            html_body: "Welcome to the app"
          )
        end
      end

      defmodule MyApp.EmailDeliveryTest do
        use ExUnit.Case
        use Bamboo.Test

        test "sends welcome email" do
          user = %User{...}
          email = MyApp.Email.welcome_email(user)

          email |> MyApp.Mailer.deliver_now

          # Works with deliver_now and deliver_later
          assert_delivered_email email
        end
      end
  """

  @doc """
  Imports Bamboo.Test and Bamboo.Formatter.format_email_address/2

  `Bamboo.Test` and the `Bamboo.TestAdapter` work by sending a message to the
  current process when an email is delivered. The process mailbox is then
  checked when using the assertion helpers like `assert_delivered_email/1`.

  Sometimes emails don't show up when asserting because you may deliver an email
  from a _different_ process than the test process. When that happens, turn on
  shared mode. This will tell `Bamboo.TestAdapter` to always send to the test process.
  This means that you cannot use shared mode with async tests.

  ## Try to use this version first

      use Bamboo.Test

  ## And if you are delivering from another process, set `shared: true`

      use Bamboo.Test, shared: true

  Common scenarios for delivering mail from a different process are when you
  send from inside of a Task, GenServer, or are running acceptance tests with a
  headless browser like phantomjs.
  """
  defmacro __using__(shared: true) do
    quote do
      setup tags do
        if tags[:async] do
          raise """
          you cannot use Bamboo.Test shared mode with async tests.

          There are a few options, the 1st is the easiest:

            1) Set your test to [async: false].
            2) If you are delivering emails from another process (for example,
               delivering from within Task.async or Process.spawn), try using
               Mailer.deliver_later. If you use Mailer.deliver_later without
               spawning another process you can use Bamboo.Test with [async:
               true] and without the shared mode.
            3) If you are writing an acceptance test that requires shared mode,
               try using a controller test instead. Then see if the test works
               without shared mode.
          """
        else
          Application.put_env(:bamboo, :shared_test_process, self())
        end

        :ok
      end

      import Bamboo.Formatter, only: [format_email_address: 2]
      import Bamboo.Test
    end
  end

  defmacro __using__(_opts) do
    quote do
      setup tags do
        Application.delete_env(:bamboo, :shared_test_process)

        :ok
      end

      import Bamboo.Formatter, only: [format_email_address: 2]
      import Bamboo.Test
    end
  end

  @doc """
  Checks whether an email was delivered.

  Must be used with the `Bamboo.TestAdapter` or this will never pass. In case you
  are delivering from another process, the assertion waits up to 100ms before
  failing. Typically if an email is successfully delivered the assertion will
  pass instantly, so test suites will remain fast.

  ## Examples

      email = Bamboo.Email.new_email(subject: "something")
      email |> MyApp.Mailer.deliver
      assert_delivered_email(email) # Will pass

      unsent_email = Bamboo.Email.new_email(subject: "something else")
      assert_delivered_email(unsent_email) # Will fail
  """
  defmacro assert_delivered_email(email) do
    quote do
      import ExUnit.Assertions
      email = Bamboo.Test.normalize_for_testing(unquote(email))
      assert_receive({:delivered_email, ^email}, 100, Bamboo.Test.flunk_with_email_list(email))
    end
  end

  @doc """
  Check whether an email's params are equal to the ones provided.

  Must be used with the `Bamboo.TestAdapter` or this will never pass. In case you
  are delivering from another process, the assertion waits up to 100ms before
  failing. Typically if an email is successfully delivered the assertion will
  pass instantly, so test suites will remain fast.

  ## Examples

      email = Bamboo.Email.new_email(subject: "something")
      email |> MyApp.Mailer.deliver
      assert_email_delivered_with(subject: "something") # Will pass

      unsent_email = Bamboo.Email.new_email(subject: "something else")
      assert_email_delivered_with(subject: "something else") # Will fail

  You can also pass a regex to match portions of an email.

  ## Example

      email = new_email(text_body: "I love coffee")
      assert_email_delivered_with(email, text_body: ~r/love/) # Will pass
      assert_email_delivered_with(email, text_body: ~r/like/) # Will fail
  """
  defmacro assert_email_delivered_with(email_params) do
    quote bind_quoted: [email_params: email_params] do
      import ExUnit.Assertions
      assert_receive({:delivered_email, email}, 100, Bamboo.Test.flunk_no_emails_received)

      recieved_email_params = email |> Map.from_struct
      assert Enum.all?(email_params, fn({k, v}) -> do_match(recieved_email_params[k], v) end),
        Bamboo.Test.flunk_attributes_do_not_match(email_params, recieved_email_params)
    end
  end

  @doc false
  def do_match(value1, value2 = %Regex{}) do
    Regex.match?(value2, value1)
  end

  @doc false
  def do_match(value1, value2) do
    value1 == value2
  end

  @doc false
  def flunk_with_email_list(email) do
    if Enum.empty?(delivered_emails()) do
      flunk_no_emails_received()
    else
      flunk """
      There were no matching emails.

      No emails matched:

        #{inspect email}

      Delivered emails:

      #{delivered_emails_as_list()}
      """
    end
  end

  @doc false
  def flunk_no_emails_received do
    flunk """
    There were 0 emails delivered to this process.

    If you expected an email to be sent, try these ideas:

    1) Make sure you call deliver_now/1 or deliver_later/1 to deliver the email
    2) Make sure you are using the Bamboo.TestAdapter
    3) Use shared mode with Bamboo.Test. This will allow Bamboo.Test
    to work across processes: use Bamboo.Test, shared: :true
    4) If you are writing an acceptance test through a headless browser, use
    shared mode as described in option 3.
    """
  end

  @doc false
  def flunk_attributes_do_not_match(params_given, params_received) do
    """
    The parameters given do not match.

      Parameters given:

        #{inspect params_given}

      Email recieved:

        #{inspect params_received}
    """
  end

  defp delivered_emails do
    {:messages, messages} = Process.info(self(), :messages)

    for {:delivered_email, _} = email_message <- messages do
      email_message
    end
  end

  defp delivered_emails_as_list do
    delivered_emails() |> add_asterisk |> Enum.join("\n")
  end

  defp add_asterisk(emails) do
    Enum.map(emails, &" * #{inspect &1}")
  end

  @doc """
  Checks that no emails were sent.

  If `Bamboo.Test` is used with shared mode, you must also configure a timeout
  in your test config.

      # Set this in your config, typically in config/test.exs
      config :bamboo, :refute_timeout, 10

  The value you set is up to you. Lower values will result in faster tests,
  but may incorrectly pass if an email is delivered *after* the timeout. Often
  times 1ms is enough.
  """
  def assert_no_emails_delivered do
    receive do
      {:delivered_email, email} -> flunk_with_unexpected_email(email)
    after
      refute_timeout() -> true
    end
  end

  @doc false
  def assert_no_emails_sent do
    raise "assert_no_emails_sent/0 has been renamed to assert_no_emails_delivered/0"
  end

  defp flunk_with_unexpected_email(email) do
    flunk """
    Unexpectedly delivered an email when expected none to be delivered.

    Delivered email:

      #{inspect email}
    """
  end

  @doc """
  Ensures a particular email was not sent

  Same as `assert_delivered_email/0`, except it checks that a particular email
  was not sent.

  If `Bamboo.Test` is used with shared mode, you must also configure a timeout
  in your test config.

      # Set this in your config, typically in config/test.exs
      config :bamboo, :refute_timeout, 10

  The value you set is up to you. Lower values will result in faster tests,
  but may incorrectly pass if an email is delivered *after* the timeout. Often
  times 1ms is enough.
  """
  def refute_delivered_email(%Bamboo.Email{} = email) do
    email = normalize_for_testing(email)

    receive do
      {:delivered_email, ^email} -> flunk_with_unexpected_matching_email(email)
    after
      refute_timeout() -> true
    end
  end

  defp flunk_with_unexpected_matching_email(email) do
    flunk """
    Unexpectedly delivered a matching email.

    Matched email that was delivered:

      #{inspect email}
    """
  end

  defp refute_timeout do
    if using_shared_mode?() do
      Application.get_env(:bamboo, :refute_timeout) || raise """
      When using shared mode with Bamboo.Test, you must set a timeout. This
      is because an email can be delivered after the assertion is called.

          # Set this in your config, typically in config/test.exs
          config :bamboo, :refute_timeout, 10

      The value you set is up to you. Lower values will result in faster tests,
      but may incorrectly pass if an email is delivered *after* the timeout.
      """
    else
      0
    end
  end

  defp using_shared_mode? do
    !!Application.get_env(:bamboo, :shared_test_process)
  end

  @doc false
  def normalize_for_testing(email) do
    email
    |> Bamboo.Mailer.normalize_addresses
    |> Bamboo.TestAdapter.clean_assigns
  end
end


Only consider this year's workshops for the participant limit

defmodule Api.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  alias Api.Repo
  alias Api.{Workshops.Workshop, Workshops.Attendance}
  alias Api.{Teams.Invite, Teams.Membership}
  alias Api.AICompetition.Bot
  alias Comeonin.Bcrypt

  @valid_attrs ~w(
    email
    name
    password
    birthday
    employment_status
    college
    company
    github_handle
    twitter_handle
    linkedin_url
    bio
    tshirt_size
    pwd_recovery_token
    pwd_recovery_token_expiration
  )a

  @admin_attrs @valid_attrs ++ ~w(
    role
  )a

  @required_attrs ~w(
    email
  )a

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "users" do
    field :email, :string
    field :name, :string
    field :password_hash, :string
    field :birthday, :date
    field :employment_status, :string
    field :college, :string
    field :company, :string
    field :github_handle, :string
    field :twitter_handle, :string
    field :linkedin_url, :string
    field :bio, :string
    field :role, :string, default: "participant"
    field :tshirt_size, :string
    field :pwd_recovery_token, :string
    field :pwd_recovery_token_expiration, :utc_datetime, default: nil

    timestamps()

    # Virtual fields
    field :password, :string, virtual: true

    # Associations
    has_many :invites, Invite, foreign_key: :host_id, on_delete: :delete_all
    has_many :invitations, Invite, foreign_key: :invitee_id, on_delete: :delete_all
    has_many :memberships, Membership, foreign_key: :user_id, on_delete: :delete_all
    has_many :teams, through: [:memberships, :team]
    has_many :ai_competition_bots, Bot

    many_to_many :workshops, Workshop, join_through: Attendance, on_delete: :delete_all
  end

  def changeset(struct, params \\ %{}),  do: _cs(struct, params, @valid_attrs)
  def participant_changeset(struct, params \\ %{}),  do: _cs(struct, params, @valid_attrs)
  def admin_changeset(struct, params \\ %{}), do: _cs(struct, params, @admin_attrs)
  defp _cs(struct, params, attrs) do
    struct
    |> cast(params, attrs)
    |> validate_required(@required_attrs)
    |> validate_length(:email, min: 1, max: 255)
    |> validate_format(:email, ~r/@/)
    |> unique_constraint(:email)
    |> unique_constraint(:password_recovery_token)
  end

  def registration_changeset(struct, params \\ %{}) do
    struct
    |> changeset(params)
    |> validate_required(~w(password)a)
    |> validate_length(:password, min: 6)
    |> hash_password
  end

  defp hash_password(%{valid?: false} = changeset), do: changeset
  defp hash_password(%{valid?: true} = changeset) do
    hashed_password =
      changeset
      |> get_field(:password)
      |> Bcrypt.hashpwsalt()

    changeset
    |> put_change(:password_hash, hashed_password)
  end

  # gravatar hash
  def gravatar_hash(%{email: email}),
    do: :crypto.hash(:md5, String.trim(email)) |> Base.encode16(case: :lower)

  def display_name(%{name: name, email: email}) do
    name || (email |> String.split("@") |> Enum.at(0))
  end

  def can_apply_to_workshops(user) do
    user = Repo.preload(user, [:workshops, :teams])

    (Enum.any?(
      user.teams, fn team -> team.applied == true &&
    end) && Enum.count(user.workshops, fn(w) -> w.year == DateTime.utc_now.year end) < 2) || true
  end

  def can_apply_to_hackathon(user) do
    user = Repo.preload(user, :workshops)

    Enum.count(user.workshops, fn(w) -> w.year == DateTime.utc_now.year end) <= 2
  end

  # generate password recovery token
  def generate_token(length \\ 32) do
    :crypto.strong_rand_bytes(length)
    |> Base.url_encode64
    |> binary_part(0, length)
  end

  def calculate_token_expiration do
    :erlang.universaltime
    |> :calendar.datetime_to_gregorian_seconds
    |> Kernel.+(30 * 60)
    |> DateTime.from_unix!
  end

  # def able_to_vote(at \\ nil) do
  #   at = at || DateTime.utc_now

  #   from(
  #     u in User,
  #     left_join: tm in assoc(u, :teams),
  #     left_join: t in assoc(tm, :team),
  #     where: u.role == "participant",
  #     where: is_nil(t.disqualified_at) or t.disqualified_at > ^at,
  #   )
  # end
end

defmodule Api.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  alias Api.Repo
  alias Api.{Workshops.Workshop, Workshops.Attendance}
  alias Api.{Teams.Invite, Teams.Membership}
  alias Api.AICompetition.Bot
  alias Comeonin.Bcrypt

  @valid_attrs ~w(
    email
    name
    password
    birthday
    employment_status
    college
    company
    github_handle
    twitter_handle
    linkedin_url
    bio
    tshirt_size
    pwd_recovery_token
    pwd_recovery_token_expiration
  )a

  @admin_attrs @valid_attrs ++ ~w(
    role
  )a

  @required_attrs ~w(
    email
  )a

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "users" do
    field :email, :string
    field :name, :string
    field :password_hash, :string
    field :birthday, :date
    field :employment_status, :string
    field :college, :string
    field :company, :string
    field :github_handle, :string
    field :twitter_handle, :string
    field :linkedin_url, :string
    field :bio, :string
    field :role, :string, default: "participant"
    field :tshirt_size, :string
    field :pwd_recovery_token, :string
    field :pwd_recovery_token_expiration, :utc_datetime, default: nil

    timestamps()

    # Virtual fields
    field :password, :string, virtual: true

    # Associations
    has_many :invites, Invite, foreign_key: :host_id, on_delete: :delete_all
    has_many :invitations, Invite, foreign_key: :invitee_id, on_delete: :delete_all
    has_many :memberships, Membership, foreign_key: :user_id, on_delete: :delete_all
    has_many :teams, through: [:memberships, :team]
    has_many :ai_competition_bots, Bot

    many_to_many :workshops, Workshop, join_through: Attendance, on_delete: :delete_all
  end

  def changeset(struct, params \\ %{}),  do: _cs(struct, params, @valid_attrs)
  def participant_changeset(struct, params \\ %{}),  do: _cs(struct, params, @valid_attrs)
  def admin_changeset(struct, params \\ %{}), do: _cs(struct, params, @admin_attrs)
  defp _cs(struct, params, attrs) do
    struct
    |> cast(params, attrs)
    |> validate_required(@required_attrs)
    |> validate_length(:email, min: 1, max: 255)
    |> validate_format(:email, ~r/@/)
    |> unique_constraint(:email)
    |> unique_constraint(:password_recovery_token)
  end

  def registration_changeset(struct, params \\ %{}) do
    struct
    |> changeset(params)
    |> validate_required(~w(password)a)
    |> validate_length(:password, min: 6)
    |> hash_password
  end

  defp hash_password(%{valid?: false} = changeset), do: changeset
  defp hash_password(%{valid?: true} = changeset) do
    hashed_password =
      changeset
      |> get_field(:password)
      |> Bcrypt.hashpwsalt()

    changeset
    |> put_change(:password_hash, hashed_password)
  end

  # gravatar hash
  def gravatar_hash(%{email: email}),
    do: :crypto.hash(:md5, String.trim(email)) |> Base.encode16(case: :lower)

  def display_name(%{name: name, email: email}) do
    name || (email |> String.split("@") |> Enum.at(0))
  end

  def can_apply_to_workshops(user) do
    user = Repo.preload(user, [:workshops, :teams])

    (Enum.any?(
      user.teams, fn team -> team.applied == true
    end) && Enum.count(user.workshops) < 2) || true
  end

  def can_apply_to_hackathon(user) do
    user = Repo.preload(user, :workshops)

    Enum.count(user.workshops) <= 2
  end

  # generate password recovery token
  def generate_token(length \\ 32) do
    :crypto.strong_rand_bytes(length)
    |> Base.url_encode64
    |> binary_part(0, length)
  end

  def calculate_token_expiration do
    :erlang.universaltime
    |> :calendar.datetime_to_gregorian_seconds
    |> Kernel.+(30 * 60)
    |> DateTime.from_unix!
  end

  # def able_to_vote(at \\ nil) do
  #   at = at || DateTime.utc_now

  #   from(
  #     u in User,
  #     left_join: tm in assoc(u, :teams),
  #     left_join: t in assoc(tm, :team),
  #     where: u.role == "participant",
  #     where: is_nil(t.disqualified_at) or t.disqualified_at > ^at,
  #   )
  # end
end


fix: Previous version bumps should not trigger minor changes

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

defmodule GoogleApis.ChangeAnalyzer do
  @moduledoc """
  Analyzes the current git diff and determines what kind of changes have been
  made. This information can be used to determine what type of version bump
  should accompany the change.
  """

  alias GoogleApis.ApiConfig

  require Logger

  @doc """
  Analyzes the git diff for the given API, and determines whether any of the
  changes are actual significant changes as opposed to documentation, comment,
  or formatting.
  """
  def has_significant_changes?(api_config) do
    dir = ApiConfig.library_directory(api_config)
    {only_modified, files} = check_git_status(dir)
    if only_modified do
      Enum.any?(files, &file_has_significant_changes?/1)
    else
      true
    end
  end

  defp check_git_status(dir) do
    case System.cmd("git", ["status", "--porcelain"]) do
      {results, 0} ->
        results
        |> String.split("\n", trim: true)
        |> Enum.reduce({true, []}, fn
          (_, {false, _}) ->
            {false, []}
          (line, {true, files}) ->
            case Regex.run(~r{^ M (\S+)}, line) do
              [_, file] ->
                if String.starts_with?(Path.expand(file), dir) do
                  {true, [file | files]}
                else
                  {true, files}
                end
              nil ->
                {false, []}
            end
        end)
      _ ->
        Logger.error("Change analyzer: git status failed!")
        {false, []}
    end
  end

  @elixir_source_extensions [".ex", ".exs"]
  @non_significant_files ["synth.metadata", "README.md"]

  defp file_has_significant_changes?(file) do
    cond do
      Enum.member?(@elixir_source_extensions, Path.extname(file)) ->
        elixir_file_has_significant_changes?(file)
      Enum.member?(@non_significant_files, Path.basename(file)) ->
        false
      true ->
        true
    end
  end

  defp elixir_file_has_significant_changes?(file) do
    with {old_content, 0} <- System.cmd("git", ["show", "HEAD:#{file}"]),
         {:ok, new_content} <- File.read(file) do
      elixir_content_has_significant_changes?(old_content, new_content, file)
    else
      {:error, _} ->
        Logger.error("Change analyzer: could not read file #{file}")
        true
      _ ->
        Logger.error("Change analyzer: git show failed for file #{file}")
        true
    end
  end

  defp elixir_content_has_significant_changes?(old_content, new_content, file) do
    with {:old, {:ok, old_ast}} <- {:old, Code.string_to_quoted(old_content)},
         {:new, {:ok, new_ast}} <- {:new, Code.string_to_quoted(new_content)} do
      elixir_ast_has_significant_changes?(old_ast, new_ast, Path.basename(file))
    else
      {:old, _} ->
        Logger.error("Change analyzer: parse of pre-change #{file} failed!")
        true
      {:new, _} ->
        Logger.error("Change analyzer: parse of post-change #{file} failed!")
        true
    end
  end

  defp elixir_ast_has_significant_changes?(old_ast, new_ast, basename) do
    strip_trivial_ast(old_ast, basename) != strip_trivial_ast(new_ast, basename)
  end

  defp strip_trivial_ast({:@, _, [{:doc, _, [str]}]}, _) when is_binary(str) do
    {:@, [], [{:doc, [], ["."]}]}
  end

  defp strip_trivial_ast({:@, _, [{:moduledoc, _, [str]}]}, _) when is_binary(str) do
    {:@, [], [{:moduledoc, [], ["."]}]}
  end

  defp strip_trivial_ast({:@, _, [{:version, _, [str]}]}, "mix.exs") when is_binary(str) do
    {:@, [], [{:version, [], ["."]}]}
  end

  defp strip_trivial_ast({name, _, args}, basename) do
    {strip_trivial_ast(name, basename), [], strip_trivial_ast(args, basename)}
  end

  defp strip_trivial_ast(list, basename) when is_list(list) do
    Enum.map(list, &(strip_trivial_ast(&1, basename)))
  end

  defp strip_trivial_ast({key, value}, basename) do
    {strip_trivial_ast(key, basename), strip_trivial_ast(value, basename)}
  end

  defp strip_trivial_ast(term, _) do
    term
  end
end

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

defmodule GoogleApis.ChangeAnalyzer do
  @moduledoc """
  Analyzes the current git diff and determines what kind of changes have been
  made. This information can be used to determine what type of version bump
  should accompany the change.
  """

  alias GoogleApis.ApiConfig

  require Logger

  @doc """
  Analyzes the git diff for the given API, and determines whether any of the
  changes are actual significant changes as opposed to documentation, comment,
  or formatting.
  """
  def has_significant_changes?(api_config) do
    dir = ApiConfig.library_directory(api_config)
    {only_modified, files} = check_git_status(dir)
    if only_modified do
      Enum.any?(files, &file_has_significant_changes?/1)
    else
      true
    end
  end

  defp check_git_status(dir) do
    case System.cmd("git", ["status", "--porcelain"]) do
      {results, 0} ->
        results
        |> String.split("\n", trim: true)
        |> Enum.reduce({true, []}, fn
          (_, {false, _}) ->
            {false, []}
          (line, {true, files}) ->
            case Regex.run(~r{^ M (\S+)}, line) do
              [_, file] ->
                if String.starts_with?(Path.expand(file), dir) do
                  {true, [file | files]}
                else
                  {true, files}
                end
              nil ->
                {false, []}
            end
        end)
      _ ->
        Logger.error("Change analyzer: git status failed!")
        {false, []}
    end
  end

  @elixir_source_extensions [".ex", ".exs"]
  @non_significant_files ["synth.metadata", "README.md"]

  defp file_has_significant_changes?(file) do
    cond do
      Enum.member?(@elixir_source_extensions, Path.extname(file)) ->
        elixir_file_has_significant_changes?(file)
      Enum.member?(@non_significant_files, Path.basename(file)) ->
        false
      true ->
        true
    end
  end

  defp elixir_file_has_significant_changes?(file) do
    with {old_content, 0} <- System.cmd("git", ["show", "HEAD:#{file}"]),
         {:ok, new_content} <- File.read(file) do
      elixir_content_has_significant_changes?(old_content, new_content, file)
    else
      {:error, _} ->
        Logger.error("Change analyzer: could not read file #{file}")
        true
      _ ->
        Logger.error("Change analyzer: git show failed for file #{file}")
        true
    end
  end

  defp elixir_content_has_significant_changes?(old_content, new_content, file) do
    with {:old, {:ok, old_ast}} <- {:old, Code.string_to_quoted(old_content)},
         {:new, {:ok, new_ast}} <- {:new, Code.string_to_quoted(new_content)} do
      elixir_ast_has_significant_changes?(old_ast, new_ast)
    else
      {:old, _} ->
        Logger.error("Change analyzer: parse of pre-change #{file} failed!")
        true
      {:new, _} ->
        Logger.error("Change analyzer: parse of post-change #{file} failed!")
        true
    end
  end

  defp elixir_ast_has_significant_changes?(old_ast, new_ast) do
    strip_trivial_ast(old_ast) != strip_trivial_ast(new_ast)
  end

  defp strip_trivial_ast({:@, _, [{:doc, _, [str]}]}) when is_binary(str) do
    {:@, [], [{:doc, [], ["."]}]}
  end

  defp strip_trivial_ast({:@, _, [{:moduledoc, _, [str]}]}) when is_binary(str) do
    {:@, [], [{:moduledoc, [], ["."]}]}
  end

  defp strip_trivial_ast({name, _, args}) do
    {strip_trivial_ast(name), [], strip_trivial_ast(args)}
  end

  defp strip_trivial_ast(list) when is_list(list) do
    Enum.map(list, &strip_trivial_ast/1)
  end

  defp strip_trivial_ast({key, value}) do
    {strip_trivial_ast(key), strip_trivial_ast(value)}
  end

  defp strip_trivial_ast(term) do
    term
  end
end