diff --git a/README.md b/README.md index 52755899..817abe9f 100644 --- a/README.md +++ b/README.md @@ -44,13 +44,15 @@ given user is a delegate, organizer or another staff member at the given competi Now to the crucial integration element - data synchronization. At the WCA we developed a standardized format for exchanging competition data between tools/systems, it's -called [WCIF](https://github.com/thewca/wcif/blob/master/specification.md) (WCA Competition Interchange Format) +called [WCIF](https://github.com/thewca/wcif/blob/latest/specification.md) (WCA Competition Interchange Format) and defines a huge JSON object that contains all relevant competition information (events, rounds, schedule, competitors, results, etc.). The idea behind it is to have a well defined data format and empower developers to build tools working with this data (more on that in the [vision](https://github.com/thewca/wcif/blob/master/vision.md) document). The WCA website offers endpoints for fetching and saving data in this -format and that's exactly what WCA Live uses. When a delegate/organizer imports a +format. WCA Live requests WCIF v2 and exports WCIF 2.2. It supports sequential +rounds; linked rounds and reserved places are rejected during import because the +current scoring model cannot represent them. When a delegate/organizer imports a competition into the app, we fetch WCIF data for this competition and save all of it into the database. Later, competition admins can trigger synchronization, which works both ways: gets new data from the WCA website saving changes to the local diff --git a/lib/wca_live/competitions/qualification.ex b/lib/wca_live/competitions/qualification.ex index 942dbad2..a840ec19 100644 --- a/lib/wca_live/competitions/qualification.ex +++ b/lib/wca_live/competitions/qualification.ex @@ -7,7 +7,7 @@ defmodule WcaLive.Competitions.Qualification do import Ecto.Changeset @required_fields [:type, :result_type, :when_date] - @optional_fields [:level] + @optional_fields [:level, :earliest_result_date] @primary_key false embedded_schema do @@ -15,6 +15,7 @@ defmodule WcaLive.Competitions.Qualification do field :result_type, :string field :level, :integer field :when_date, :date + field :earliest_result_date, :date end def changeset(qualification, attrs) do diff --git a/lib/wca_live/synchronization/export.ex b/lib/wca_live/synchronization/export.ex index 913476ad..1c598800 100644 --- a/lib/wca_live/synchronization/export.ex +++ b/lib/wca_live/synchronization/export.ex @@ -27,7 +27,7 @@ defmodule WcaLive.Synchronization.Export do defp competition_to_wcif(competition) do %{ - "formatVersion" => "1.0", + "formatVersion" => "2.2", "id" => competition.wca_id, "name" => competition.name, "shortName" => competition.short_name, @@ -87,7 +87,7 @@ defmodule WcaLive.Synchronization.Export do %{ "eventId" => personal_best.event_id, "type" => personal_best.type, - "best" => personal_best.best, + "value" => personal_best.best, "worldRanking" => personal_best.world_ranking, "continentalRanking" => personal_best.continental_ranking, "nationalRanking" => personal_best.national_ranking @@ -102,28 +102,39 @@ defmodule WcaLive.Synchronization.Export do competition_event.qualification && competition_event.qualification |> qualification_to_wcif(), "rounds" => - competition_event.rounds |> Enum.sort_by(& &1.number) |> Enum.map(&round_to_wcif/1) + competition_event.rounds + |> Enum.sort_by(& &1.number) + |> Enum.map(fn round -> + previous = Enum.find(competition_event.rounds, &(&1.number == round.number - 1)) + round_to_wcif(round, previous) + end) } end defp qualification_to_wcif(qualification) do %{ - "type" => qualification.type, - "resultType" => qualification.result_type, - "level" => qualification.level, - "whenDate" => qualification.when_date |> Date.to_iso8601() + "resultCondition" => %{ + "type" => if(qualification.type == "ranking", do: "ranking", else: "resultAchieved"), + "scope" => qualification.result_type, + "value" => if(qualification.type == "anyResult", do: nil, else: qualification.level) + }, + "latestResultDate" => qualification.when_date |> Date.to_iso8601(), + "earliestResultDate" => + qualification.earliest_result_date && Date.to_iso8601(qualification.earliest_result_date) } end - defp round_to_wcif(round) do + defp round_to_wcif(round, previous) do %{ "id" => round_wcif_id(round), + "linkedRounds" => nil, "format" => round.format_id, "timeLimit" => round.time_limit && round.time_limit |> time_limit_to_wcif(), "cutoff" => round.cutoff && round.cutoff |> cutoff_to_wcif(), - "advancementCondition" => - round.advancement_condition && - round.advancement_condition |> advancement_condition_to_wcif(), + "participationRuleset" => %{ + "participationSource" => participation_source_to_wcif(round, previous), + "reservedPlaces" => nil + }, "results" => round.results |> Enum.map(&result_to_wcif/1), "scrambleSetCount" => round.scramble_set_count # "scrambleSets" => [] # ignored for now @@ -147,15 +158,28 @@ defmodule WcaLive.Synchronization.Export do defp cutoff_to_wcif(cutoff) do %{ - "attemptResult" => cutoff.attempt_result, + "resultValue" => cutoff.attempt_result, "numberOfAttempts" => cutoff.number_of_attempts } end - defp advancement_condition_to_wcif(advancement_condition) do + defp participation_source_to_wcif(%{number: 1}, _previous), do: %{"type" => "registrations"} + defp participation_source_to_wcif(_round, nil), do: nil + defp participation_source_to_wcif(_round, %{advancement_condition: nil}), do: nil + + defp participation_source_to_wcif(_round, previous) do + condition = previous.advancement_condition + format = WcaLive.Wca.Format.get_by_id!(previous.format_id) + %{ - "type" => advancement_condition.type, - "level" => advancement_condition.level + "type" => "round", + "roundId" => round_wcif_id(previous), + "resultCondition" => %{ + "type" => + if(condition.type == "attemptResult", do: "resultAchieved", else: condition.type), + "scope" => if(format.sort_by == :best, do: "single", else: "average"), + "value" => condition.level + } } end @@ -171,7 +195,7 @@ defmodule WcaLive.Synchronization.Export do defp attempt_to_wcif(attempt) do %{ - "result" => attempt.result, + "value" => attempt.result, "reconstruction" => attempt.reconstruction } end diff --git a/lib/wca_live/synchronization/import.ex b/lib/wca_live/synchronization/import.ex index b7b05dbb..9c142117 100644 --- a/lib/wca_live/synchronization/import.ex +++ b/lib/wca_live/synchronization/import.ex @@ -24,7 +24,8 @@ defmodule WcaLive.Synchronization.Import do def import_competition(competition, wcif) do Repo.transaction_with( fn -> - with {:ok, competition} <- insert_or_update_competition(competition, wcif), + with :ok <- validate_supported_wcif(wcif), + {:ok, competition} <- insert_or_update_competition(competition, wcif), :ok <- insert_new_users(wcif), {:ok, competition} <- update_competition_staff_members(competition, wcif), {:ok, competition} <- update_competition_events(competition, wcif), @@ -47,6 +48,12 @@ defmodule WcaLive.Synchronization.Import do @spec import_results(%Competition{}, map(), %User{}) :: {:ok, %Competition{}} | {:error, any()} def import_results(competition, wcif, user) do + with :ok <- validate_supported_wcif(wcif) do + do_import_results(competition, wcif, user) + end + end + + defp do_import_results(competition, wcif, user) do Repo.transaction_with(fn -> competition = Repo.preload(competition, [:people, competition_events: [rounds: :results]]) @@ -74,6 +81,56 @@ defmodule WcaLive.Synchronization.Import do end) end + defp validate_supported_wcif(%{"formatVersion" => "2." <> _version} = wcif) do + error = + Enum.find_value(wcif["events"], fn + %{"id" => "fto"} -> "FTO is not supported by WCA Live" + event -> Enum.find_value(event["rounds"], &unsupported_round_message(&1, event["rounds"])) + end) + + if error, do: {:error, error}, else: :ok + end + + defp validate_supported_wcif(_wcif), do: {:error, "expected WCIF version 2"} + + defp unsupported_round_message(round, rounds) do + source = get_in(round, ["participationRuleset", "participationSource"]) + + cond do + round["linkedRounds"] not in [nil, []] or source["type"] == "linkedRounds" -> + "Dual rounds are only supported by the WCA's Integrated Live Results platform" + + get_in(round, ["participationRuleset", "reservedPlaces"]) != nil -> + "Reserved places are not supported by WCA Live (#{round["id"]})" + + not supported_source?(source, round, rounds) -> + "Unsupported participation source or result condition in #{round["id"]}" + + true -> + nil + end + end + + defp supported_source?(nil, _round, _rounds), do: true + + defp supported_source?(%{"type" => "registrations"}, round, _rounds) do + Wcif.ActivityCode.parse!(round["id"]).round_number == 1 + end + + defp supported_source?(%{"type" => "round"} = source, round, rounds) do + %{event_id: event_id, round_number: number} = Wcif.ActivityCode.parse!(round["id"]) + previous = Enum.find(rounds, &(&1["id"] == source["roundId"])) + condition = source["resultCondition"] + sort_by = previous && Wca.Format.get_by_id!(previous["format"]).sort_by + scope = if sort_by == :best, do: "single", else: "average" + + previous != nil and source["roundId"] == "#{event_id}-r#{number - 1}" and + condition["type"] in ["ranking", "percent", "resultAchieved"] and + condition["scope"] == scope and is_integer(condition["value"]) + end + + defp supported_source?(_source, _round, _rounds), do: false + @doc """ Returns round information for every round taht has no results, but whose WCIF round has results. @@ -108,7 +165,7 @@ defmodule WcaLive.Synchronization.Import do person = Enum.find(competition.people, &(&1.registrant_id == wcif_result["personId"])) do attempts = Enum.map(wcif_result["attempts"], fn attempt -> - %{result: attempt["result"], reconstruction: attempt["reconstruction"]} + %{result: attempt["value"], reconstruction: attempt["reconstruction"]} end) %Scoretaking.Result{} @@ -367,7 +424,7 @@ defmodule WcaLive.Synchronization.Import do |> Repo.update() end - defp competition_event_changeset(competition_event, wcif_event, competition) do + defp competition_event_changeset(competition_event, wcif_event, _competition) do competition_event = Repo.preload(competition_event, :rounds) competition_event @@ -377,7 +434,7 @@ defmodule WcaLive.Synchronization.Import do qualification: wcif_event["qualification"] |> wcif_qualification_to_attrs() }) |> build_assoc(:rounds, wcif_event["rounds"], - with: &round_changeset(&1, &2, competition), + with: &round_changeset(&1, &2, wcif_event["rounds"]), equality: fn round, wcif_round -> %{event_id: event_id, round_number: number} = Wcif.ActivityCode.parse!(wcif_round["id"]) competition_event.event_id == event_id and round.number == number @@ -388,23 +445,37 @@ defmodule WcaLive.Synchronization.Import do defp wcif_qualification_to_attrs(nil), do: nil defp wcif_qualification_to_attrs(wcif_qualification) do + condition = wcif_qualification["resultCondition"] + %{ - type: wcif_qualification["type"], - result_type: wcif_qualification["resultType"], - level: wcif_qualification["level"], - when_date: wcif_qualification["whenDate"] + type: qualification_type(condition), + result_type: condition["scope"], + level: condition["value"], + when_date: wcif_qualification["latestResultDate"], + earliest_result_date: wcif_qualification["earliestResultDate"] } end - defp round_changeset(round, wcif_round, _competition) do + defp qualification_type(%{"type" => "resultAchieved", "value" => nil}), do: "anyResult" + defp qualification_type(%{"type" => "resultAchieved"}), do: "attemptResult" + defp qualification_type(condition), do: condition["type"] + + defp round_changeset(round, wcif_round, wcif_rounds) do + %{event_id: event_id, round_number: number} = Wcif.ActivityCode.parse!(wcif_round["id"]) + + next_round = + Enum.find(wcif_rounds, &(&1["id"] == "#{event_id}-r#{number + 1}")) + + condition = + get_in(next_round, ["participationRuleset", "participationSource", "resultCondition"]) + round |> Scoretaking.Round.changeset(%{ - number: wcif_round["id"] |> Wcif.ActivityCode.parse!() |> Map.fetch!(:round_number), + number: number, format_id: wcif_round["format"], time_limit: wcif_round["timeLimit"] |> wcif_time_limit_to_attrs(), cutoff: wcif_round["cutoff"] |> wcif_cutoff_to_attrs(), - advancement_condition: - wcif_round["advancementCondition"] |> wcif_advancement_condition_to_attrs(), + advancement_condition: condition |> wcif_advancement_condition_to_attrs(), scramble_set_count: wcif_round["scrambleSetCount"] }) end @@ -422,16 +493,20 @@ defmodule WcaLive.Synchronization.Import do defp wcif_cutoff_to_attrs(wcif_cutoff) do %{ - attempt_result: wcif_cutoff["attemptResult"], + attempt_result: wcif_cutoff["resultValue"], number_of_attempts: wcif_cutoff["numberOfAttempts"] } end defp wcif_advancement_condition_to_attrs(nil), do: nil + defp wcif_advancement_condition_to_attrs(%{"type" => "resultAchieved", "value" => value}) do + %{level: value, type: "attemptResult"} + end + defp wcif_advancement_condition_to_attrs(wcif_advancement_condition) do %{ - level: wcif_advancement_condition["level"], + level: wcif_advancement_condition["value"], type: wcif_advancement_condition["type"] } end @@ -580,7 +655,7 @@ defmodule WcaLive.Synchronization.Import do |> Competitions.PersonalBest.changeset(%{ event_id: wcif_personal_best["eventId"], type: wcif_personal_best["type"], - best: wcif_personal_best["best"], + best: wcif_personal_best["value"], world_ranking: wcif_personal_best["worldRanking"], continental_ranking: wcif_personal_best["continentalRanking"], national_ranking: wcif_personal_best["nationalRanking"] diff --git a/lib/wca_live/wca/api.ex b/lib/wca_live/wca/api.ex index d70ce76b..1a5b8ba9 100644 --- a/lib/wca_live/wca/api.ex +++ b/lib/wca_live/wca/api.ex @@ -37,7 +37,7 @@ defmodule WcaLive.Wca.Api do build_req() |> with_user_token(access_token) |> with_long_timeout() - |> request(url: "/competitions/#{competition_wca_id}/wcif") + |> request(url: "/competitions/#{competition_wca_id}/wcif/version/2") end @doc """ diff --git a/test/wca_live/synchronization/export_test.exs b/test/wca_live/synchronization/export_test.exs index 7cbecedb..c5d621c2 100644 --- a/test/wca_live/synchronization/export_test.exs +++ b/test/wca_live/synchronization/export_test.exs @@ -5,7 +5,7 @@ defmodule WcaLive.Synchronization.ExportTest do alias WcaLive.Synchronization.Export @wcif %{ - "formatVersion" => "1.0", + "formatVersion" => "2.2", "id" => "WC2019", "name" => "WCA World Championship 2019", "shortName" => "WCA WC 2019", @@ -42,7 +42,7 @@ defmodule WcaLive.Synchronization.ExportTest do "personalBests" => [ %{ "eventId" => "333", - "best" => 790, + "value" => 790, "type" => "single", "worldRanking" => 995, "continentalRanking" => 105, @@ -64,19 +64,22 @@ defmodule WcaLive.Synchronization.ExportTest do }, "cutoff" => %{ "numberOfAttempts" => 2, - "attemptResult" => 3000 + "resultValue" => 3000 + }, + "linkedRounds" => nil, + "participationRuleset" => %{ + "participationSource" => %{"type" => "registrations"}, + "reservedPlaces" => nil }, - "advancementCondition" => nil, "results" => [], "scrambleSetCount" => 4 } ], "competitorLimit" => nil, "qualification" => %{ - "type" => "ranking", - "resultType" => "average", - "level" => 50, - "whenDate" => "2019-07-01" + "resultCondition" => %{"type" => "ranking", "scope" => "average", "value" => 50}, + "latestResultDate" => "2019-07-01", + "earliestResultDate" => nil } } ], diff --git a/test/wca_live/synchronization/import_results_test.exs b/test/wca_live/synchronization/import_results_test.exs index 74a9598a..96261b92 100644 --- a/test/wca_live/synchronization/import_results_test.exs +++ b/test/wca_live/synchronization/import_results_test.exs @@ -3,6 +3,7 @@ defmodule WcaLive.Synchronization.ImportResultsTest do import WcaLive.Factory alias WcaLive.Synchronization.Import + alias WcaLive.Synchronization.Export alias WcaLive.Repo test "import_results/3 imports results for an empty round" do @@ -17,7 +18,7 @@ defmodule WcaLive.Synchronization.ImportResultsTest do wcif_result(7, [3232, 3213, 2323, 2323, 2323]) ]) - assert {:ok, _competition} = Import.import_results(competition, wcif, user) + assert {:ok, competition} = Import.import_results(competition, wcif, user) [result] = round |> Ecto.assoc(:results) |> Repo.all() @@ -28,6 +29,15 @@ defmodule WcaLive.Synchronization.ImportResultsTest do assert result.best == 2323 assert result.average == 2620 assert result.ranking == 1 + + [event] = Export.export_competition(Repo.reload!(competition))["events"] + [exported_round] = event["rounds"] + [exported_result] = exported_round["results"] + + assert Enum.map(exported_result["attempts"], & &1["value"]) == [3232, 3213, 2323, 2323, 2323] + + assert exported_result["best"] == 2323 + assert exported_result["average"] == 2620 end test "import_results/3 skips rounds that already have results" do @@ -106,6 +116,8 @@ defmodule WcaLive.Synchronization.ImportResultsTest do person = insert(:person, competition: competition, registrant_id: 7) wcif = %{ + "formatVersion" => "2.2", + "id" => "TestCompetition", "events" => [ %{ "id" => "333", @@ -139,6 +151,8 @@ defmodule WcaLive.Synchronization.ImportResultsTest do defp build_wcif(event_id, round_number, format_id, results) do %{ + "formatVersion" => "2.2", + "id" => "TestCompetition", "events" => [ %{ "id" => event_id, @@ -156,7 +170,7 @@ defmodule WcaLive.Synchronization.ImportResultsTest do defp wcif_result(person_id, attempts) do attempt_objects = - Enum.map(attempts, fn result -> %{"result" => result, "reconstruction" => nil} end) + Enum.map(attempts, fn result -> %{"value" => result, "reconstruction" => nil} end) %{ "personId" => person_id, diff --git a/test/wca_live/synchronization/import_test.exs b/test/wca_live/synchronization/import_test.exs index aabfb623..334ce8ad 100644 --- a/test/wca_live/synchronization/import_test.exs +++ b/test/wca_live/synchronization/import_test.exs @@ -4,10 +4,11 @@ defmodule WcaLive.Synchronization.ImportTest do alias WcaLive.Competitions.Competition alias WcaLive.Synchronization.Import + alias WcaLive.Synchronization.Export alias WcaLive.Repo @wcif %{ - "formatVersion" => "1.0", + "formatVersion" => "2.2", "id" => "WC2019", "name" => "WCA World Championship 2019", "shortName" => "WCA WC 2019", @@ -45,7 +46,7 @@ defmodule WcaLive.Synchronization.ImportTest do "personalBests" => [ %{ "eventId" => "333", - "best" => 790, + "value" => 790, "type" => "single", "worldRanking" => 995, "continentalRanking" => 105, @@ -53,7 +54,7 @@ defmodule WcaLive.Synchronization.ImportTest do }, %{ "eventId" => "333", - "best" => 855, + "value" => 855, "type" => "average", "worldRanking" => 590, "continentalRanking" => 78, @@ -75,9 +76,13 @@ defmodule WcaLive.Synchronization.ImportTest do }, "cutoff" => %{ "numberOfAttempts" => 2, - "attemptResult" => 3000 + "resultValue" => 3000 + }, + "linkedRounds" => nil, + "participationRuleset" => %{ + "participationSource" => %{"type" => "registrations"}, + "reservedPlaces" => nil }, - "advancementCondition" => nil, "results" => [], "scrambleSetCount" => 4, # Ignored until WCA API supports it @@ -87,10 +92,9 @@ defmodule WcaLive.Synchronization.ImportTest do ], "competitorLimit" => nil, "qualification" => %{ - "type" => "ranking", - "resultType" => "average", - "level" => 50, - "whenDate" => "2019-07-01" + "resultCondition" => %{"type" => "ranking", "scope" => "average", "value" => 50}, + "latestResultDate" => "2019-07-01", + "earliestResultDate" => nil }, "extensions" => [] } @@ -157,6 +161,193 @@ defmodule WcaLive.Synchronization.ImportTest do assert "WCA World Championship 2019" == competition.name end + for {type, internal_type} <- [ + {"ranking", "ranking"}, + {"percent", "percent"}, + {"resultAchieved", "attemptResult"} + ] do + test "import/export maps #{type} advancement to the destination round" do + [event] = @wcif["events"] + [first] = event["rounds"] + first = %{first | "format" => "3"} + + second = %{ + first + | "id" => "333-r2", + "format" => "a", + "cutoff" => nil, + "participationRuleset" => %{ + "participationSource" => %{ + "type" => "round", + "roundId" => "333-r1", + "resultCondition" => %{"type" => unquote(type), "scope" => "single", "value" => 50} + }, + "reservedPlaces" => nil + } + } + + third = %{ + second + | "id" => "333-r3", + "participationRuleset" => %{ + "participationSource" => %{ + "type" => "round", + "roundId" => "333-r2", + "resultCondition" => %{"type" => unquote(type), "scope" => "average", "value" => 10} + }, + "reservedPlaces" => nil + } + } + + # Round ordering in the incoming JSON must not determine advancement. + wcif = put_in(@wcif, ["events", Access.at(0), "rounds"], [third, first, second]) + competition = insert(:competition, wca_id: "WC2019") + + assert {:ok, competition} = Import.import_competition(competition, wcif) + + event = + competition |> Ecto.assoc(:competition_events) |> Repo.one!() |> Repo.preload(:rounds) + + [round1, round2, round3] = Enum.sort_by(event.rounds, & &1.number) + assert round1.advancement_condition.type == unquote(internal_type) + assert round1.advancement_condition.level == 50 + assert round2.advancement_condition.level == 10 + assert round3.advancement_condition == nil + + [exported_event] = Export.export_competition(competition)["events"] + + assert exported_event["rounds"] == + [first, second, third] |> Enum.map(&Map.delete(&1, "extensions")) + end + end + + for {type, value, internal_type} <- [ + {"ranking", 50, "ranking"}, + {"resultAchieved", 1000, "attemptResult"}, + {"resultAchieved", nil, "anyResult"} + ] do + test "#{internal_type} qualification and dates survive import/export" do + competition = insert(:competition, wca_id: "WC2019") + + qualification = %{ + "resultCondition" => %{ + "type" => unquote(type), + "scope" => "single", + "value" => unquote(value) + }, + "latestResultDate" => "2019-07-01", + "earliestResultDate" => "2018-01-01" + } + + wcif = put_in(@wcif, ["events", Access.at(0), "qualification"], qualification) + assert {:ok, competition} = Import.import_competition(competition, wcif) + event = competition |> Ecto.assoc(:competition_events) |> Repo.one!() + assert event.qualification.type == unquote(internal_type) + [exported_event] = Export.export_competition(competition)["events"] + assert exported_event["qualification"] == qualification + end + end + + test "FTO is rejected before importing a competition" do + user = insert(:user) + [event] = @wcif["events"] + [round] = event["rounds"] + fto_event = %{event | "id" => "fto", "rounds" => [%{round | "id" => "fto-r1"}]} + wcif = %{@wcif | "events" => [event, fto_event]} + + assert {:error, "FTO is not supported by WCA Live"} = + Import.import_competition(%Competition{imported_by: user}, wcif) + + refute Repo.exists?(Competition) + end + + test "linked rounds fail before changing the competition" do + competition = insert(:competition, wca_id: "WC2019", name: "Original name") + + wcif = + put_in(@wcif, ["events", Access.at(0), "rounds", Access.at(0), "linkedRounds"], [ + "333-r1", + "333-r2" + ]) + + assert {:error, + "Dual rounds are only supported by the WCA's Integrated Live Results platform"} = + Import.import_competition(competition, wcif) + + assert Repo.reload!(competition).name == "Original name" + end + + test "linked-round participation sources fail before changing the competition" do + competition = insert(:competition, wca_id: "WC2019", name: "Original name") + + wcif = + put_in( + @wcif, + [ + "events", + Access.at(0), + "rounds", + Access.at(0), + "participationRuleset", + "participationSource" + ], + %{ + "type" => "linkedRounds", + "roundIds" => ["333-r1", "333-r2"] + } + ) + + assert {:error, + "Dual rounds are only supported by the WCA's Integrated Live Results platform"} = + Import.import_competition(competition, wcif) + + assert Repo.reload!(competition).name == "Original name" + end + + test "reserved places fail before changing the competition" do + competition = insert(:competition, wca_id: "WC2019", name: "Original name") + + wcif = + put_in( + @wcif, + [ + "events", + Access.at(0), + "rounds", + Access.at(0), + "participationRuleset", + "reservedPlaces" + ], + %{ + "nationalities" => ["GB"], + "count" => 3 + } + ) + + assert {:error, "Reserved places are not supported by WCA Live (333-r1)"} = + Import.import_competition(competition, wcif) + + assert Repo.reload!(competition).name == "Original name" + end + + test "registration participation sources are rejected after the first round" do + competition = insert(:competition, wca_id: "WC2019", name: "Original name") + wcif = put_in(@wcif, ["events", Access.at(0), "rounds", Access.at(0), "id"], "333-r2") + + assert {:error, "Unsupported participation source or result condition in 333-r2"} = + Import.import_competition(competition, wcif) + + assert Repo.reload!(competition).name == "Original name" + end + + test "WCIF v1 is rejected before changing the competition" do + competition = insert(:competition, wca_id: "WC2019", name: "Original name") + wcif = %{@wcif | "formatVersion" => "1.0"} + + assert {:error, "expected WCIF version 2"} = Import.import_competition(competition, wcif) + assert Repo.reload!(competition).name == "Original name" + end + test "import_competition/2 imports nested objects into the corresponding associations" do # That's not a very detailed test in terms of possible cases, # but it briefly checks that all the associations are saved properly. diff --git a/test/wca_live_web/controllers/competition_controller_test.exs b/test/wca_live_web/controllers/competition_controller_test.exs index c6399fee..e76a1347 100644 --- a/test/wca_live_web/controllers/competition_controller_test.exs +++ b/test/wca_live_web/controllers/competition_controller_test.exs @@ -14,7 +14,7 @@ defmodule WcaLiveWeb.CompetitionControllerTest do conn = get(conn, "/api/competitions/#{competition.id}/wcif") body = json_response(conn, 200) - assert %{"formatVersion" => "1.0", "id" => "WC2019"} = body + assert %{"formatVersion" => "2.2", "id" => "WC2019"} = body end test "returns error when not signed in", %{conn: conn} do