From 624a6c643ec445048dfe463019b445ca4f68ec82 Mon Sep 17 00:00:00 2001 From: daniel_sp Date: Thu, 11 Jan 2024 22:26:46 +0000 Subject: [PATCH] chore: drop the pairings feature --- lib/bokken/pairings.ex | 140 ----------- lib/bokken/utils/hungarian_algorithm.ex | 191 --------------- .../controllers/pairing_controller.ex | 22 -- lib/bokken_web/router.ex | 2 - lib/bokken_web/views/pairing_view.ex | 23 -- test/bokken/pairings_test.exs | 231 ------------------ .../controllers/pairing_controller_test.exs | 49 ---- 7 files changed, 658 deletions(-) delete mode 100644 lib/bokken/pairings.ex delete mode 100644 lib/bokken/utils/hungarian_algorithm.ex delete mode 100644 lib/bokken_web/controllers/pairing_controller.ex delete mode 100644 lib/bokken_web/views/pairing_view.ex delete mode 100644 test/bokken/pairings_test.exs delete mode 100644 test/bokken_web/controllers/pairing_controller_test.exs diff --git a/lib/bokken/pairings.ex b/lib/bokken/pairings.ex deleted file mode 100644 index ace784fd..00000000 --- a/lib/bokken/pairings.ex +++ /dev/null @@ -1,140 +0,0 @@ -defmodule Bokken.Pairings do - @moduledoc """ - The Pairings context. - """ - - import Ecto.Query, warn: false - - alias Bokken.{Events, HungarianAlgorithm, Repo} - - @doc """ - Creates lectures for an event. - ## Examples - iex> create_pairings(123) - [%Lecture{}] - """ - def create_pairings(event_id) do - available_mentors = get_available_mentors(event_id) - available_ninjas = get_available_ninjas(event_id) - - if available_mentors == [] or available_ninjas == [] do - [] - else - pairings_table = pairings_table(available_ninjas, available_mentors) - - matches = - create_matrix(pairings_table) - |> HungarianAlgorithm.compute() - - create_lectures(pairings_table, matches, event_id) - end - end - - @doc """ - Gets all available mentors for an event. - ## Examples - iex> get_available_mentors(123) - [%Mentor{}] - """ - def get_available_mentors(event_id) do - query = - from m in Bokken.Accounts.Mentor, - join: a in Bokken.Events.Availability, - on: m.id == a.mentor_id, - where: a.is_available and a.event_id == ^event_id, - preload: [:skills] - - Repo.all(query) - end - - @doc """ - Gets all available ninjas for an event. - ## Examples - iex> get_available_ninjas(123) - [%Ninja{}] - """ - def get_available_ninjas(event_id) do - query = - from n in Bokken.Accounts.Ninja, - join: e in Bokken.Events.Enrollment, - on: n.id == e.ninja_id, - where: e.accepted and e.event_id == ^event_id, - preload: [:skills] - - Repo.all(query) - end - - # Creates a table of pairings between ninjas and mentors to be used after the - # hungarian algorirthm returns all the matches. - defp pairings_table([], _rest_mentors), do: [] - - defp pairings_table([ninja | rest_ninjas], mentors) do - row = - Enum.map(mentors, fn mentor -> - %{ninja: ninja, mentor: mentor, score: 100 - score(ninja.skills, mentor.skills)} - end) - - [row | pairings_table(rest_ninjas, mentors)] - end - - # Returns a score of comapatibility between a ninja and a mentor bases on the - # percentage of skills matching. - # If the ninja and the mentor have the exact same skills then the score is 100. - defp score(_ninja_skills, []), do: 0 - defp score([], _mentor_skills), do: 0 - - defp score(ninja_skills, mentor_skills) do - matching_skills = matching_skills(ninja_skills, mentor_skills) - - total_skills = - Enum.concat(ninja_skills, mentor_skills) - |> Enum.frequencies() - |> Enum.count() - - if matching_skills == 0 do - 0 - else - round(matching_skills / total_skills * 100) - end - end - - # This function helps the score() function to determine the skills that match - # between the ninja and the mentor. - defp matching_skills(ninja_skills, mentor_skills) do - ninja_skills - |> Enum.filter(fn ns -> Enum.member?(mentor_skills, ns) end) - |> length() - end - - # In order to simplify the format of the matrix that will run in the - # Hungarian algorirthm, this function creates a matrix with just the scores - # of the pairings table. - defp create_matrix([]), do: [] - - defp create_matrix([row | rest]) do - [aux_create_matrix(row) | create_matrix(rest)] - end - - defp aux_create_matrix(row) do - Enum.map(row, fn pair -> pair.score end) - end - - # When the Hungarian algorirthm finishes, this function creates all the lectures - # for the resulted matches. - defp create_lectures(_pairings_table, [], _event_id), do: [] - - defp create_lectures(pairings_table, [{c, r} | rest], event_id) do - pairing = - Enum.at(pairings_table, c) - |> Enum.at(r) - - attrs = %{ - ninja_id: pairing.ninja.id, - mentor_id: pairing.mentor.id, - event_id: event_id - } - - {:ok, lecture} = Events.create_lecture(attrs) - [lecture | create_lectures(pairings_table, rest, event_id)] - end -end diff --git a/lib/bokken/utils/hungarian_algorithm.ex b/lib/bokken/utils/hungarian_algorithm.ex deleted file mode 100644 index 536ac4f2..00000000 --- a/lib/bokken/utils/hungarian_algorithm.ex +++ /dev/null @@ -1,191 +0,0 @@ -defmodule Bokken.HungarianAlgorithm do - @moduledoc """ - The Hungarian Algorithm module. - """ - - # takes an nxn matrix of costs and returns a list of {row, column} - # tuples of assigments that minimizes total cost - def compute([row1 | _] = matrix) do - matrix - # add "zero" rows if its not a square matrix - |> pad() - # perform the calculation - |> step() - # remove any assignments that are in the padded matrix - |> Enum.filter(fn {r, c} -> r < length(matrix) and c < length(row1) end) - end - - defp step(matrix, step \\ 1, assignments \\ nil, count \\ 0) - - # match on done - defp step(matrix, _step, assignments, _count) when length(assignments) == length(matrix), - do: assignments - - # For each row of the matrix, find the smallest element and - # subtract it from every element in its row. If no assignments, go step 2 - defp step(matrix, 1, _assignments, _count) do - transformed = rows_to_zero(matrix) - assigned = assignments(transformed) - step(transformed, 2, assigned) - end - - # For each column of the matrix, find the smallest element and - # subtract it from every element in its column. If no assignments, go step 3 - defp step(matrix, 2, _assignments, _count) do - transformed = - matrix - |> transpose() - |> rows_to_zero() - |> transpose() - - assigned = assignments(transformed) - step(transformed, 3, assigned) - end - - defp step(matrix, 3, _assignments, count) do - {covered_rows, covered_cols} = min_lines(matrix) - - min_uncovered = - matrix - |> transform(fn {r, c}, val -> - if c not in covered_cols and r not in covered_rows do - val - end - end) - |> List.flatten() - |> Enum.filter(&(!is_nil(&1))) - |> Enum.min() - - transformed = - matrix - |> transform(fn {r, c}, val -> - case {r in covered_rows, c in covered_cols} do - # if uncovered, subtract the min - {false, false} -> val - min_uncovered - # if covered by a vertical and horizontal line, add min_uncovered - {true, true} -> val + min_uncovered - # otherwise, leave it alone - _ -> val - end - end) - - assigned = assignments(transformed) - - if count < 50 do - step(transformed, 3, assigned, count + 1) - else - raise "Could not handle the input matrix." - end - end - - defp assignments(matrix) do - matrix - |> reduce([], fn {r, c} = coord, val, acc -> - if val == 0 do - h_zeros = row(matrix, r) |> Enum.count(&(&1 == 0)) - v_zeros = column(matrix, c) |> Enum.count(&(&1 == 0)) - [{coord, h_zeros + v_zeros} | acc] - else - acc - end - end) - |> Enum.sort_by(fn {_, zero_count} -> zero_count end) - |> Enum.reduce([], fn {{r, c} = coord, _}, acc -> - {assigned_rows, assigned_cols} = Enum.unzip(acc) - - if r not in assigned_rows && c not in assigned_cols do - [coord | acc] - else - acc - end - end) - end - - defp min_lines(matrix) do - matrix - # Calculate the max number of zeros vertically vs horizontally for each xy position in the input matrix - # and store the result in a separate array called m2. - # While calculating, if horizontal zeros > vertical zeroes, then the calculated number is converted - # to negative. (just to distinguish which direction we chose for later use) - |> transform(fn {r, c}, val -> - h_zeros = row(matrix, r) |> Enum.count(&(&1 == 0)) - v_zeros = column(matrix, c) |> Enum.count(&(&1 == 0)) - - cond do - val != 0 -> 0 - h_zeros > v_zeros -> -h_zeros - true -> v_zeros - end - end) - # Loop through all elements in the m2 array. If the value is positive, draw a vertical line in array m3, - # if value is negative, draw an horizontal line in m3 - |> reduce({[], []}, fn - {_, c}, val, {rows, cols} when val > 0 -> {rows, [c | cols] |> Enum.uniq()} - {r, _}, val, {rows, cols} when val < 0 -> {[r | rows] |> Enum.uniq(), cols} - _, _, acc -> acc - end) - end - - defp rows_to_zero(matrix) do - Enum.map(matrix, fn row -> - min = Enum.min(row) - - Enum.map(row, fn column -> - column - min - end) - end) - end - - defp transpose(matrix) do - transform(matrix, fn {r, c}, _ -> matrix |> Enum.at(c) |> Enum.at(r) end) - end - - defp transform(matrix, func) do - matrix - |> Enum.with_index() - |> Enum.map(fn {row, r} -> - row - |> Enum.with_index() - |> Enum.map(fn {_column, c} -> - func.({r, c}, matrix |> Enum.at(r) |> Enum.at(c)) - end) - end) - end - - def reduce(matrix, init, func) do - matrix - |> Enum.with_index() - |> Enum.reduce(init, fn {row, r}, acc -> - row - |> Enum.with_index() - |> Enum.reduce(acc, fn {_column, c}, acc2 -> - func.({r, c}, matrix |> Enum.at(r) |> Enum.at(c), acc2) - end) - end) - end - - defp row(matrix, index), do: Enum.at(matrix, index) - defp column(matrix, index), do: Enum.map(matrix, &Enum.at(&1, index)) - - defp pad([first | _] = matrix) do - case length(matrix) - length(first) do - # use the matrix only if it has the same number of columns and rows - 0 -> - matrix - - # more rows than columns, add zero columns to each row - diff when diff > 0 -> - Enum.map(matrix, fn row -> add_zero_columns(row, diff) end) - - # more columns than rows, add a row of zeros - diff when diff < 0 -> - matrix ++ [Enum.map(1..length(matrix), fn _ -> 0 end)] - end - end - - defp pad(matrix), do: matrix - - defp add_zero_columns(row, diff) do - row ++ Enum.map(1..abs(diff), fn _ -> 0 end) - end -end diff --git a/lib/bokken_web/controllers/pairing_controller.ex b/lib/bokken_web/controllers/pairing_controller.ex deleted file mode 100644 index 624ae16d..00000000 --- a/lib/bokken_web/controllers/pairing_controller.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule BokkenWeb.PairingController do - use BokkenWeb, controller: "1.6" - - alias Bokken.Events - alias Bokken.Pairings - - def index(conn, %{"event_id" => event_id}) when is_organizer(conn) do - lectures = Events.get_lectures_from_event(event_id) - - conn - |> put_status(:ok) - |> render("index.json", lectures: lectures) - end - - def create(conn, %{"event_id" => event_id}) when is_organizer(conn) do - lectures = Pairings.create_pairings(event_id) - - conn - |> put_status(:created) - |> render("create.json", lectures: lectures) - end -end diff --git a/lib/bokken_web/router.ex b/lib/bokken_web/router.ex index 27b8db6d..5c0efa8b 100644 --- a/lib/bokken_web/router.ex +++ b/lib/bokken_web/router.ex @@ -121,8 +121,6 @@ defmodule BokkenWeb.Router do resources "/enrollments", EnrollmentController, except: [:new, :edit] resources "/availabilities", AvailabilityController, except: [:new, :edit, :delete] - - resources "/pairings", PairingController, only: [:index, :create] end resources "/lectures", LectureController, except: [:new, :edit] diff --git a/lib/bokken_web/views/pairing_view.ex b/lib/bokken_web/views/pairing_view.ex deleted file mode 100644 index dc8a893f..00000000 --- a/lib/bokken_web/views/pairing_view.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule BokkenWeb.PairingView do - use BokkenWeb, :view - - alias BokkenWeb.PairingView - - def render("index.json", %{lectures: lectures}) do - %{data: render_many(lectures, PairingView, "pairing.json")} - end - - def render("create.json", %{lectures: lectures}) do - %{data: render_many(lectures, PairingView, "pairing.json")} - end - - def render("pairing.json", %{pairing: lecture}) do - %{ - event_id: lecture.event_id, - mentor_id: lecture.mentor_id, - ninja_id: lecture.ninja_id, - notes: lecture.notes, - attendance: lecture.attendance - } - end -end diff --git a/test/bokken/pairings_test.exs b/test/bokken/pairings_test.exs deleted file mode 100644 index bd43b05e..00000000 --- a/test/bokken/pairings_test.exs +++ /dev/null @@ -1,231 +0,0 @@ -defmodule Bokken.PairingsTest do - use Bokken.DataCase - - import Bokken.Factory - - describe "pairings" do - alias Bokken.Pairings - - defp forget(struct, field, cardinality \\ :one) do - %{ - struct - | field => %Ecto.Association.NotLoaded{ - __field__: field, - __owner__: struct.__struct__, - __cardinality__: cardinality - } - } - end - - test "get_available_mentors/1 returns all available mentors for an event" do - event = insert(:event) - skill = insert(:skill) - - mentor = - insert(:mentor, %{skills: [skill]}) - |> forget(:user) - - attrs = %{event: event, is_available: true, mentor: mentor} - - insert(:availability, attrs) - - result = Pairings.get_available_mentors(event.id) - - assert result == [mentor] - end - - test "get_available_ninjas/1 returns all available ninjas for an event" do - event = insert(:event) - skill = insert(:skill) - - ninja = - insert(:ninja, %{skills: [skill]}) - |> forget(:guardian) - |> forget(:user) - - attrs = %{event: event, accepted: true, ninja: ninja} - - insert(:enrollment, attrs) - - result = Pairings.get_available_ninjas(event.id) - - assert result == [ninja] - end - end - - describe "Hungarian algorithm" do - alias Bokken.Pairings - - test "create_pairings/1 returns lectures with matching skills" do - event = insert(:event) - - skill1 = insert(:skill) - skill2 = insert(:skill) - skill3 = insert(:skill) - - ninja1 = insert(:ninja, %{skills: [skill1, skill2]}) - ninja2 = insert(:ninja, %{skills: [skill2, skill3]}) - ninja3 = insert(:ninja, %{skills: [skill1, skill3]}) - - mentor1 = insert(:mentor, %{skills: [skill1, skill2]}) - mentor2 = insert(:mentor, %{skills: [skill2, skill3]}) - mentor3 = insert(:mentor, %{skills: [skill1, skill3]}) - - insert(:availability, %{mentor: mentor1, event: event, is_available: true}) - insert(:availability, %{mentor: mentor2, event: event, is_available: true}) - insert(:availability, %{mentor: mentor3, event: event, is_available: true}) - - insert(:enrollment, %{ninja: ninja1, event: event, accepted: true}) - insert(:enrollment, %{ninja: ninja2, event: event, accepted: true}) - insert(:enrollment, %{ninja: ninja3, event: event, accepted: true}) - - insert(:lecture, %{ninja: ninja1, mentor: mentor1}) - insert(:lecture, %{ninja: ninja2, mentor: mentor2}) - insert(:lecture, %{ninja: ninja3, mentor: mentor3}) - - [lecture_1, lecture_2, lecture_3] = Pairings.create_pairings(event.id) - - assert lecture_1.mentor_id == mentor1.id - assert lecture_2.mentor_id == mentor2.id - assert lecture_3.mentor_id == mentor3.id - - assert lecture_1.ninja_id == ninja1.id - assert lecture_2.ninja_id == ninja2.id - assert lecture_3.ninja_id == ninja3.id - end - - test "create_pairings/1 returns lectures without matching skills" do - event = insert(:event) - - skill1 = insert(:skill) - skill2 = insert(:skill) - skill3 = insert(:skill) - - ninja1 = insert(:ninja) - ninja2 = insert(:ninja, %{skills: [skill2, skill3]}) - ninja3 = insert(:ninja, %{skills: [skill1, skill3]}) - - mentor1 = insert(:mentor, %{skills: [skill1, skill2]}) - mentor2 = insert(:mentor, %{skills: [skill2, skill3]}) - mentor3 = insert(:mentor, %{skills: [skill3]}) - - insert(:availability, %{mentor: mentor1, event: event, is_available: true}) - insert(:availability, %{mentor: mentor2, event: event, is_available: true}) - insert(:availability, %{mentor: mentor3, event: event, is_available: true}) - - insert(:enrollment, %{ninja: ninja1, event: event, accepted: true}) - insert(:enrollment, %{ninja: ninja2, event: event, accepted: true}) - insert(:enrollment, %{ninja: ninja3, event: event, accepted: true}) - - insert(:lecture, %{ninja: ninja1, mentor: mentor1}) - insert(:lecture, %{ninja: ninja2, mentor: mentor2}) - insert(:lecture, %{ninja: ninja3, mentor: mentor3}) - - [lecture_1, lecture_2, lecture_3] = Pairings.create_pairings(event.id) - - assert lecture_1.mentor_id == mentor1.id - assert lecture_2.mentor_id == mentor2.id - assert lecture_3.mentor_id == mentor3.id - - assert lecture_1.ninja_id == ninja1.id - assert lecture_2.ninja_id == ninja2.id - assert lecture_3.ninja_id == ninja3.id - end - - test "create_pairings/1 for when number of ninjas is higher then mentors" do - event = insert(:event) - - skill1 = insert(:skill) - skill2 = insert(:skill) - skill3 = insert(:skill) - - ninja1 = insert(:ninja, %{skills: [skill1, skill2]}) - ninja2 = insert(:ninja, %{skills: [skill2, skill3]}) - ninja3 = insert(:ninja, %{skills: [skill1, skill3]}) - - mentor1 = insert(:mentor, %{skills: [skill1, skill2]}) - mentor2 = insert(:mentor, %{skills: [skill2, skill3]}) - - insert(:availability, %{mentor: mentor1, event: event, is_available: true}) - insert(:availability, %{mentor: mentor2, event: event, is_available: true}) - - insert(:enrollment, %{ninja: ninja1, event: event, accepted: true}) - insert(:enrollment, %{ninja: ninja2, event: event, accepted: true}) - insert(:enrollment, %{ninja: ninja3, event: event, accepted: true}) - - [lecture_1, lecture_2] = Pairings.create_pairings(event.id) - - assert lecture_1.mentor_id == mentor1.id - assert lecture_2.mentor_id == mentor2.id - - assert lecture_1.ninja_id == ninja1.id - assert lecture_2.ninja_id == ninja2.id - end - - test "create_pairings/1 for when number of mentors is higher then ninjas" do - event = insert(:event) - - skill1 = insert(:skill) - skill2 = insert(:skill) - skill3 = insert(:skill) - - ninja1 = insert(:ninja, %{skills: [skill1, skill2]}) - ninja2 = insert(:ninja, %{skills: [skill2, skill3]}) - - mentor1 = insert(:mentor, %{skills: [skill1, skill2]}) - mentor2 = insert(:mentor, %{skills: [skill2, skill3]}) - mentor3 = insert(:mentor, %{skills: [skill2, skill3]}) - - insert(:availability, %{mentor: mentor1, event: event, is_available: true}) - insert(:availability, %{mentor: mentor2, event: event, is_available: true}) - insert(:availability, %{mentor: mentor3, event: event, is_available: true}) - - insert(:enrollment, %{ninja: ninja1, event: event, accepted: true}) - insert(:enrollment, %{ninja: ninja2, event: event, accepted: true}) - - [lecture_1, lecture_2] = Pairings.create_pairings(event.id) - - assert lecture_1.mentor_id == mentor1.id - assert lecture_2.mentor_id == mentor3.id - - assert lecture_1.ninja_id == ninja1.id - assert lecture_2.ninja_id == ninja2.id - end - - test "create_pairings/1 when an event doesn't have ninjas" do - event = insert(:event) - - skill1 = insert(:skill) - skill2 = insert(:skill) - skill3 = insert(:skill) - - mentor1 = insert(:mentor, %{skills: [skill1, skill2]}) - mentor2 = insert(:mentor, %{skills: [skill2, skill3]}) - mentor3 = insert(:mentor, %{skills: [skill1, skill3]}) - - insert(:availability, %{mentor: mentor1, event: event, is_available: true}) - insert(:availability, %{mentor: mentor2, event: event, is_available: true}) - insert(:availability, %{mentor: mentor3, event: event, is_available: true}) - - assert Pairings.create_pairings(event.id) == [] - end - - test "create_pairings/1 when an event doesn't have mentors" do - event = insert(:event) - - skill1 = insert(:skill) - skill2 = insert(:skill) - skill3 = insert(:skill) - - ninja1 = insert(:ninja, %{skills: [skill1, skill2]}) - ninja2 = insert(:ninja, %{skills: [skill2, skill3]}) - ninja3 = insert(:ninja, %{skills: [skill1, skill3]}) - - insert(:enrollment, %{ninja: ninja1, event: event, accepted: true}) - insert(:enrollment, %{ninja: ninja2, event: event, accepted: true}) - insert(:enrollment, %{ninja: ninja3, event: event, accepted: true}) - - assert Pairings.create_pairings(event.id) == [] - end - end -end diff --git a/test/bokken_web/controllers/pairing_controller_test.exs b/test/bokken_web/controllers/pairing_controller_test.exs deleted file mode 100644 index 7ca8f03f..00000000 --- a/test/bokken_web/controllers/pairing_controller_test.exs +++ /dev/null @@ -1,49 +0,0 @@ -defmodule BokkenWeb.PairingControllerTest do - use BokkenWeb.ConnCase - - import Bokken.Factory - - describe "create" do - setup [:login_as_organizer] - - test "create pairings for an event", %{conn: conn} do - event = insert(:event) - - skill1 = insert(:skill) - skill2 = insert(:skill) - skill3 = insert(:skill) - - ninja1 = insert(:ninja, %{skills: [skill1, skill2]}) - ninja2 = insert(:ninja, %{skills: [skill2, skill3]}) - ninja3 = insert(:ninja, %{skills: [skill1, skill3]}) - - mentor1 = insert(:mentor, %{skills: [skill1, skill2]}) - mentor2 = insert(:mentor, %{skills: [skill2, skill3]}) - mentor3 = insert(:mentor, %{skills: [skill1, skill3]}) - - insert(:availability, %{mentor: mentor1, event: event, is_available: true}) - insert(:availability, %{mentor: mentor2, event: event, is_available: true}) - insert(:availability, %{mentor: mentor3, event: event, is_available: true}) - - insert(:enrollment, %{ninja: ninja1, event: event, accepted: true}) - insert(:enrollment, %{ninja: ninja2, event: event, accepted: true}) - insert(:enrollment, %{ninja: ninja3, event: event, accepted: true}) - - conn = post(conn, Routes.event_pairing_path(conn, :create, event.id)) - assert Enum.count(json_response(conn, 201)["data"]) == 3 - end - end - - describe "index" do - setup [:login_as_organizer] - - test "list all pairings for an event", %{conn: conn} do - event = insert(:event) - - insert_list(3, :lecture, %{event: event}) - - conn = get(conn, Routes.event_pairing_path(conn, :index, event.id)) - assert Enum.count(json_response(conn, 200)["data"]) == 3 - end - end -end