diff --git a/lib/dev_round/changeset.ex b/lib/dev_round/changeset.ex index fe42598..3a0c989 100644 --- a/lib/dev_round/changeset.ex +++ b/lib/dev_round/changeset.ex @@ -157,4 +157,31 @@ defmodule DevRound.Changeset do end end end + + @doc """ + Validates that given field contains a valid HTTP or HTTPS URL. + + ## Examples + + iex> changeset = %Ecto.Changeset{changes: %{url: "http://example.com"}} + iex> Changeset.validate_http_url(changeset, :url) + # No error added + + iex> changeset = %Ecto.Changeset{changes: %{url: "invalid"}} + iex> Changeset.validate_http_url(changeset, :url) + # Adds error to url field + + """ + def validate_http_url(changeset, field) do + validate_change(changeset, field, fn field, url -> + case URI.parse(url) do + %URI{scheme: scheme, host: host} + when scheme in ["http", "https"] and not is_nil(host) and host != "" -> + [] + + _ -> + [{field, "Must be a valid HTTP(S) URL."}] + end + end) + end end diff --git a/lib/dev_round/events.ex b/lib/dev_round/events.ex index 44349e9..615d936 100644 --- a/lib/dev_round/events.ex +++ b/lib/dev_round/events.ex @@ -105,7 +105,13 @@ defmodule DevRound.Events do sessions_query = from s in EventSession, order_by: s.begin event - |> Repo.preload([:langs, :hosts, :last_live_session, sessions: sessions_query]) + |> Repo.preload([ + :langs, + :hosts, + :last_live_session, + :team_video_conference_rooms, + sessions: sessions_query + ]) |> Repo.preload(events_attendees: {attendee_query, [:user, :langs]}) end diff --git a/lib/dev_round/events/event.ex b/lib/dev_round/events/event.ex index 03614f8..3d68404 100644 --- a/lib/dev_round/events/event.ex +++ b/lib/dev_round/events/event.ex @@ -31,9 +31,14 @@ defmodule DevRound.Events.Event do field :slides_page_number, :integer field :live, :boolean field :modified_at, :utc_datetime + field :main_video_conference_room_url, :string many_to_many :langs, Lang, join_through: "event_langs", on_replace: :delete + has_many :team_video_conference_rooms, DevRound.Events.TeamVideoConferenceRoom, + on_replace: :delete, + on_delete: :delete_all + has_many :event_hosts, EventHost, preload_order: [asc: :position], on_replace: :delete, @@ -60,7 +65,8 @@ defmodule DevRound.Events.Event do :location, :published, :registration_deadline_local, - :slides_filename + :slides_filename, + :main_video_conference_room_url ]) |> cast_assoc(:event_hosts, with: &EventHost.changeset/3, @@ -76,6 +82,11 @@ defmodule DevRound.Events.Event do sort_param: :sessions_order, drop_param: :sessions_delete ) + |> cast_assoc(:team_video_conference_rooms, + with: &DevRound.Events.TeamVideoConferenceRoom.changeset/2, + sort_param: :team_video_conference_rooms_order, + drop_param: :team_video_conference_rooms_delete + ) |> put_langs_assoc(Keyword.get(opts, :put_langs)) |> validate_required( [ @@ -107,6 +118,7 @@ defmodule DevRound.Events.Event do _ -> [] end end) + |> validate_http_url(:main_video_conference_room_url) |> unique_constraint(:slug) |> change(modified_at: DateTime.utc_now(:second)) end diff --git a/lib/dev_round/events/team_video_conference_room.ex b/lib/dev_round/events/team_video_conference_room.ex new file mode 100644 index 0000000..0cc1158 --- /dev/null +++ b/lib/dev_round/events/team_video_conference_room.ex @@ -0,0 +1,18 @@ +defmodule DevRound.Events.TeamVideoConferenceRoom do + use Ecto.Schema + import Ecto.Changeset + import DevRound.Changeset + alias DevRound.Events.Event + + schema "team_video_conference_rooms" do + field :url, :string + belongs_to :event, Event + end + + def changeset(room, attrs) do + room + |> cast(attrs, [:url]) + |> validate_required([:url]) + |> validate_http_url(:url) + end +end diff --git a/lib/dev_round/hosting.ex b/lib/dev_round/hosting.ex index 0b67247..26c1082 100644 --- a/lib/dev_round/hosting.ex +++ b/lib/dev_round/hosting.ex @@ -124,11 +124,11 @@ defmodule DevRound.Hosting do EventAttendee.check_changeset(attendee, %{checked: checked}) end - def validate_team_generation_constraints(attendees, team_names) do + def validate_team_generation_constraints(attendees, team_names, team_rooms) do attendees = filter_checked(attendees) if Enum.count(attendees) >= 2 do - messages = build_validation_messages(attendees, team_names) + messages = build_validation_messages(attendees, team_names, team_rooms) case messages do [] -> {:ok, []} @@ -139,7 +139,7 @@ defmodule DevRound.Hosting do end end - defp build_validation_messages(attendees, team_names) do + defp build_validation_messages(attendees, team_names, team_rooms) do attendee_messages = for attendee <- attendees do potential_team_mates = @@ -162,7 +162,18 @@ defmodule DevRound.Hosting do [] end - attendee_messages ++ team_names_message + remote_attendees = Enum.filter(attendees, & &1.is_remote) + + video_conference_rooms_message = + if Integer.floor_div(length(remote_attendees), 2) > length(team_rooms) do + [ + "Not enough session video conference room URLs to build teams for checked remote participants." + ] + else + [] + end + + attendee_messages ++ team_names_message ++ video_conference_rooms_message end defp filter_checked(attendees) do @@ -197,29 +208,34 @@ defmodule DevRound.Hosting do |> Enum.into(%{}, fn team -> {team.session_id, team} end) end - def build_teams_for_session(%EventSession{} = session, attendees, team_names) do + def build_teams_for_session(%EventSession{} = session, attendees, team_names, team_rooms) do attendees = filter_checked(attendees) - {:ok, []} = validate_team_generation_constraints(attendees, team_names) + {:ok, []} = validate_team_generation_constraints(attendees, team_names, team_rooms) Multi.new() |> Multi.delete_all(:teams, Ecto.assoc(session, :teams)) - |> insert_teams(session, attendees, team_names) + |> insert_teams(session, attendees, team_names, team_rooms) |> Repo.transaction() end - defp insert_teams(multi, session, attendees, team_names) do - generate_team_changesets(session, attendees, team_names) + defp insert_teams(multi, session, attendees, team_names, team_rooms) do + generate_team_changesets(session, attendees, team_names, team_rooms) |> Enum.reduce(multi, &Multi.insert(&2, Changeset.get_change(&1, :slug), &1)) end - defp generate_team_changesets(session, attendees, team_names) do + defp generate_team_changesets(session, attendees, team_names, team_rooms) do {local_teams, local_langs} = generate_teams_langs(attendees, false) {remote_teams, remote_langs} = generate_teams_langs(attendees, true) + room_urls = team_rooms |> Enum.map(& &1.url) + local_room_urls = List.duplicate(nil, length(local_teams)) + remote_room_urls = Enum.take(room_urls, length(remote_teams)) + create_team_changesets( {local_teams ++ remote_teams, local_langs ++ remote_langs}, session, - team_names + team_names, + local_room_urls ++ remote_room_urls ) end @@ -293,11 +309,11 @@ defmodule DevRound.Hosting do MapSet.new() end - defp create_team_changesets({teams_attendees, teams_langs}, session, team_names) do + defp create_team_changesets({teams_attendees, teams_langs}, session, team_names, room_urls) do names = team_names |> Enum.shuffle() |> Enum.take(length(teams_attendees)) - Enum.zip([teams_attendees, teams_langs, names]) - |> Enum.map(fn {team_attendees, team_langs, name} -> + Enum.zip([teams_attendees, teams_langs, names, room_urls]) + |> Enum.map(fn {team_attendees, team_langs, name, room_url} -> lang = Enum.random(team_langs) is_remote = hd(team_attendees).is_remote @@ -318,7 +334,8 @@ defmodule DevRound.Hosting do slug: name.slug, is_remote: is_remote, session: session, - lang: lang + lang: lang, + video_conference_room_url: room_url ) |> Changeset.put_assoc(:members, members) end) diff --git a/lib/dev_round/hosting/team.ex b/lib/dev_round/hosting/team.ex index 829f73d..3065c2d 100644 --- a/lib/dev_round/hosting/team.ex +++ b/lib/dev_round/hosting/team.ex @@ -17,6 +17,7 @@ defmodule DevRound.Hosting.Team do field :name, :string field :slug, :string field :is_remote, :boolean, default: false + field :video_conference_room_url, :string belongs_to :session, EventSession belongs_to :lang, Lang @@ -28,7 +29,7 @@ defmodule DevRound.Hosting.Team do @doc false def changeset(team, attrs) do team - |> cast(attrs, [:name, :slug, :is_remote]) + |> cast(attrs, [:name, :slug, :is_remote, :video_conference_room_url]) |> validate_required([:name, :slug, :is_remote]) end end diff --git a/lib/dev_round_web/admin/event.ex b/lib/dev_round_web/admin/event.ex index 4370732..1167eb8 100644 --- a/lib/dev_round_web/admin/event.ex +++ b/lib/dev_round_web/admin/event.ex @@ -52,6 +52,15 @@ defmodule DevRoundWeb.Admin.Event do ] end + @impl Backpex.LiveResource + def panels do + [ + content: "Content", + video_conference_rooms: "Video Conference Rooms", + settings: "Settings" + ] + end + @impl Backpex.LiveResource def fields do [ @@ -59,6 +68,22 @@ defmodule DevRoundWeb.Admin.Event do module: Backpex.Fields.Text, label: "Title" }, + event_hosts: %{ + module: DevRoundWeb.Admin.Fields.InlineCRUD, + label: "Hosts", + type: :assoc, + child_fields: [ + user: %{ + module: DevRoundWeb.Admin.Fields.BelongsTo, + label: "User", + display_field: :full_name, + live_resource: DevRoundWeb.Admin.User, + prompt: "Select user", + options_query: fn query, _field -> query |> order_by(asc: :full_name) end + } + ], + except: [:index] + }, begin_local: %{ module: Backpex.Fields.DateTime, label: "Begin" @@ -86,22 +111,7 @@ defmodule DevRoundWeb.Admin.Event do live_resource: DevRoundWeb.Admin.Lang, prompt: "Select", not_found_text: "No languages found", - except: [:index] - }, - event_hosts: %{ - module: DevRoundWeb.Admin.Fields.InlineCRUD, - label: "Hosts", - type: :assoc, - child_fields: [ - user: %{ - module: DevRoundWeb.Admin.Fields.BelongsTo, - label: "User", - display_field: :full_name, - live_resource: DevRoundWeb.Admin.User, - prompt: "Select user", - options_query: fn query, _field -> query |> order_by(asc: :full_name) end - } - ], + panel: :content, except: [:index] }, teaser: %{ @@ -109,6 +119,7 @@ defmodule DevRoundWeb.Admin.Event do label: "Teaser", help_text: "Shown on event listing page.", rows: 5, + panel: :content, except: [:index] }, body: %{ @@ -116,6 +127,7 @@ defmodule DevRoundWeb.Admin.Event do label: "Body", help_text: "Markdown is supported.", rows: 15, + panel: :content, except: [:index] }, slides_filename: %{ @@ -141,6 +153,7 @@ defmodule DevRoundWeb.Admin.Event do

""" end, + panel: :content, except: [:index] }, sessions: %{ @@ -161,11 +174,33 @@ defmodule DevRoundWeb.Admin.Event do module: Backpex.Fields.DateTime, label: "End" } - ] + ], + panel: :content + }, + main_video_conference_room_url: %{ + module: Backpex.Fields.URL, + label: "Main Video Conference Room URL", + panel: :video_conference_rooms, + except: [:index] + }, + team_video_conference_rooms: %{ + module: Backpex.Fields.InlineCRUD, + type: :assoc, + label: "Team Video Conference Rooms", + child_fields: [ + url: %{ + module: Backpex.Fields.URL, + label: "URL" + } + ], + help_text: "Used for teams with remote attendees.", + panel: :video_conference_rooms, + except: [:index] }, published: %{ module: Backpex.Fields.Boolean, - label: "Published" + label: "Published", + panel: :settings } ] end diff --git a/lib/dev_round_web/admin/item_actions/duplicate_event_action.ex b/lib/dev_round_web/admin/item_actions/duplicate_event_action.ex index 2222a53..f939bc3 100644 --- a/lib/dev_round_web/admin/item_actions/duplicate_event_action.ex +++ b/lib/dev_round_web/admin/item_actions/duplicate_event_action.ex @@ -85,11 +85,21 @@ defmodule DevRoundWeb.Admin.ItemActions.DuplicateEventAction do |> Map.merge(data) |> shift_event_dates(date_diff) |> Map.put(:sessions, Enum.map(item.sessions, &process_session(&1, date_diff))) + |> Map.put( + :event_hosts, + Enum.map(item.event_hosts, &Map.take(&1, [:event_id, :user_id, :position])) + ) + |> Map.put( + :team_video_conference_rooms, + Enum.map(item.team_video_conference_rooms, &Map.take(&1, [:url])) + ) |> Map.put(:published, false) opts = [ put_langs: item.langs, - put_hosts: item.hosts + put_hosts: item.hosts, + put_team_video_conference_rooms: + Enum.map(item.team_video_conference_rooms, &Map.take(&1, [:url])) ] case Events.create_event(attrs, opts) do diff --git a/lib/dev_round_web/components/core_components.ex b/lib/dev_round_web/components/core_components.ex index 1ae011b..a67a0b9 100644 --- a/lib/dev_round_web/components/core_components.ex +++ b/lib/dev_round_web/components/core_components.ex @@ -788,4 +788,17 @@ defmodule DevRoundWeb.CoreComponents do

""" end + + attr :href, :string + attr :class, :any, default: nil, doc: "additional classes for the link element" + slot :inner_block + + def external_link(assigns) do + ~H""" + <.link href={@href} target="_blank" class={["inline-flex gap-1 items-center", @class]}> + {render_slot(@inner_block)} + <.icon name="hero-arrow-top-right-on-square" class="w-[1em] h-[1em] opacity-70" /> + + """ + end end diff --git a/lib/dev_round_web/components/event_components.ex b/lib/dev_round_web/components/event_components.ex index e8cf74f..f14dd3e 100644 --- a/lib/dev_round_web/components/event_components.ex +++ b/lib/dev_round_web/components/event_components.ex @@ -70,6 +70,7 @@ defmodule DevRoundWeb.EventComponents do attr :show_member_experience_level, :boolean, required: true attr :show_member_langs, :boolean, required: true attr :multiple_langs, :boolean, required: true + attr :show_video_conference_room_url, :boolean, default: false attr :class, :string, default: nil attr :zoom, :float, default: nil @@ -78,15 +79,15 @@ defmodule DevRoundWeb.EventComponents do

- - + + {@team.name} + -

@@ -116,6 +117,15 @@ defmodule DevRoundWeb.EventComponents do <% end %> <% end %>
+
+ <.external_link + :if={@show_video_conference_room_url && @team.video_conference_room_url} + href={@team.video_conference_room_url} + class="link link-info" + > + Video Conference + +
""" diff --git a/lib/dev_round_web/live/event_live/show.html.heex b/lib/dev_round_web/live/event_live/show.html.heex index ee17e61..4314626 100644 --- a/lib/dev_round_web/live/event_live/show.html.heex +++ b/lib/dev_round_web/live/event_live/show.html.heex @@ -94,6 +94,12 @@
{@event.location} + <%= if @event.main_video_conference_room_url do %> + / + <.external_link href={@event.main_video_conference_room_url} class="link"> + Video Conference + + <% end %>
<%= if not @archived do %>
@@ -129,7 +135,7 @@
<%= if @pdf_url do %> - <.link href={@pdf_url} class="flex gap-1 font-semibold" target="_blank"> + <.link href={@pdf_url} class="flex gap-1 font-semibold link" target="_blank"> <.icon name="hero-arrow-down-tray" class="h-5 w-5" /> Download <% else %> diff --git a/lib/dev_round_web/live/event_slides_live/show.html.heex b/lib/dev_round_web/live/event_slides_live/show.html.heex index 942ffca..04e15c0 100644 --- a/lib/dev_round_web/live/event_slides_live/show.html.heex +++ b/lib/dev_round_web/live/event_slides_live/show.html.heex @@ -60,6 +60,15 @@ <.usage_hint> - You can follow the presentation through the slides here. Audio is not provided. + You can follow the presentation through the slides here. + <%= if @event.main_video_conference_room_url do %> + Join the + <.external_link href={@event.main_video_conference_room_url} class="link"> + video conference + + for full video/audio content. + <% else %> + Audio is not provided. + <% end %> diff --git a/lib/dev_round_web/live/hosting_base.ex b/lib/dev_round_web/live/hosting_base.ex index 69f0ddd..c6f45e3 100644 --- a/lib/dev_round_web/live/hosting_base.ex +++ b/lib/dev_round_web/live/hosting_base.ex @@ -38,7 +38,8 @@ defmodule DevRoundWeb.HostingBase do {_, messages} = Hosting.validate_team_generation_constraints( socket.assigns.event.events_attendees, - socket.assigns.team_names + socket.assigns.team_names, + socket.assigns.event.team_video_conference_rooms ) assign(socket, :messages, messages) diff --git a/lib/dev_round_web/live/hosting_session_live/show.ex b/lib/dev_round_web/live/hosting_session_live/show.ex index 07625c5..395f19e 100644 --- a/lib/dev_round_web/live/hosting_session_live/show.ex +++ b/lib/dev_round_web/live/hosting_session_live/show.ex @@ -83,7 +83,12 @@ defmodule DevRoundWeb.HostingSessionLive.Show do false = session.teams_locked {:ok, _} = - Hosting.build_teams_for_session(session, event.events_attendees, team_names) + Hosting.build_teams_for_session( + session, + event.events_attendees, + team_names, + event.team_video_conference_rooms + ) broadcast_teams_build(session) diff --git a/lib/dev_round_web/live/hosting_session_live/show.html.heex b/lib/dev_round_web/live/hosting_session_live/show.html.heex index 33b8b54..3e00edb 100644 --- a/lib/dev_round_web/live/hosting_session_live/show.html.heex +++ b/lib/dev_round_web/live/hosting_session_live/show.html.heex @@ -90,6 +90,7 @@ show_member_experience_level={true} show_member_langs={true} multiple_langs={@multiple_langs} + show_video_conference_room_url={true} /> <% end %> diff --git a/lib/dev_round_web/live/user_events_live.ex b/lib/dev_round_web/live/user_events_live.ex index 3ad9b1f..ce13626 100644 --- a/lib/dev_round_web/live/user_events_live.ex +++ b/lib/dev_round_web/live/user_events_live.ex @@ -49,7 +49,12 @@ defmodule DevRoundWeb.UserEventsLive do Up Next
- <.event_card :for={event <- @underway_events} event={event} teams_map={@teams_map} /> + <.event_card + :for={event <- @underway_events} + event={event} + teams_map={@teams_map} + show_video_conference_room_url={true} + />
@@ -97,6 +102,7 @@ defmodule DevRoundWeb.UserEventsLive do attr :teams_map, :map, required: true attr :collapsable, :boolean, default: false attr :expanded, :boolean, default: true + attr :show_video_conference_room_url, :boolean, default: false defp event_card(assigns) do ~H""" @@ -151,6 +157,7 @@ defmodule DevRoundWeb.UserEventsLive do show_member_experience_level={false} show_member_langs={false} multiple_langs={tl(@event.langs) != []} + show_video_conference_room_url={@show_video_conference_room_url} class="border border-base-content/5" /> <% else %> diff --git a/priv/repo/migrations/20260325195246_add_video_conference_rooms.exs b/priv/repo/migrations/20260325195246_add_video_conference_rooms.exs new file mode 100644 index 0000000..c08249a --- /dev/null +++ b/priv/repo/migrations/20260325195246_add_video_conference_rooms.exs @@ -0,0 +1,20 @@ +defmodule DevRound.Repo.Migrations.AddVideoConferenceRooms do + use Ecto.Migration + + def change do + alter table(:events) do + add :main_video_conference_room_url, :string + end + + alter table(:teams) do + add :video_conference_room_url, :string + end + + create table(:team_video_conference_rooms) do + add :url, :string, null: false + add :event_id, references(:events, on_delete: :delete_all), null: false + end + + create index(:team_video_conference_rooms, [:event_id]) + end +end diff --git a/test/dev_round/changeset_test.exs b/test/dev_round/changeset_test.exs index 4447549..f5d2014 100644 --- a/test/dev_round/changeset_test.exs +++ b/test/dev_round/changeset_test.exs @@ -310,4 +310,59 @@ defmodule DevRound.ChangesetTest do refute Map.has_key?(changeset.changes, :slug) end end + + describe "validate_http_url/2" do + test "accepts valid http url" do + data = %{} + types = %{url: :string} + params = %{url: "http://example.com"} + + changeset = + {data, types} + |> Ecto.Changeset.cast(params, [:url]) + |> DevRound.Changeset.validate_http_url(:url) + + refute changeset.errors[:url] + end + + test "accepts valid https url" do + data = %{} + types = %{url: :string} + params = %{url: "https://example.com"} + + changeset = + {data, types} + |> Ecto.Changeset.cast(params, [:url]) + |> DevRound.Changeset.validate_http_url(:url) + + refute changeset.errors[:url] + end + + test "rejects invalid url scheme" do + data = %{} + types = %{url: :string} + params = %{url: "ftp://example.com"} + + changeset = + {data, types} + |> Ecto.Changeset.cast(params, [:url]) + |> DevRound.Changeset.validate_http_url(:url) + + assert %{errors: [url: {"Must be a valid HTTP(S) URL.", _}]} = changeset + end + + test "rejects invalid url (no host)" do + data = %{} + types = %{url: :string} + params = %{url: "http://"} + + changeset = + {data, types} + |> Ecto.Changeset.cast(params, [:url]) + |> Ecto.Changeset.validate_required([:url]) + |> DevRound.Changeset.validate_http_url(:url) + + assert %{errors: [url: {"Must be a valid HTTP(S) URL.", _}]} = changeset + end + end end diff --git a/test/dev_round/hosting_test.exs b/test/dev_round/hosting_test.exs index b6b715a..cb22530 100644 --- a/test/dev_round/hosting_test.exs +++ b/test/dev_round/hosting_test.exs @@ -182,7 +182,13 @@ defmodule DevRound.HostingTest do attendees = [r1, r2, r3, r4, r5, l1, l2, l3, l4, l5, u1_remote, u2_local] - assert {:ok, _} = Hosting.build_teams_for_session(session, attendees, team_names) + # We need at least as many rooms as remote teams (5 remote -> 3 teams needed: 2 + 3, wait, 5 remote? `Integer.floor_div(5, 2) = 2`? No, 5 remote participants in 2 categories (Split into [A, B] and [C, D, E])), wait `order_attendees_by_experience` sorts them first. + # 5 remote participants -> 3 teams. 3 rooms needed. + team_rooms = + for _ <- 1..3, do: %DevRound.Events.TeamVideoConferenceRoom{url: "http://room.com"} + + assert {:ok, _} = + Hosting.build_teams_for_session(session, attendees, team_names, team_rooms) teams = Hosting.list_teams_for_session(session) @@ -240,13 +246,13 @@ defmodule DevRound.HostingTest do u1 = register_attendee(event, "u1", false, [lang1]) assert {:error, ["Not enough checked participants to build teams."]} = - Hosting.validate_team_generation_constraints([u1], team_names) + Hosting.validate_team_generation_constraints([u1], team_names, []) # 2. No compatible mate (different remote status) u2 = register_attendee(event, "u2", true, [lang1]) assert {:error, messages} = - Hosting.validate_team_generation_constraints([u1, u2], team_names) + Hosting.validate_team_generation_constraints([u1, u2], team_names, []) assert Enum.any?(messages, fn m -> m =~ "No team mate for u1" end) assert Enum.any?(messages, fn m -> m =~ "No team mate for u2" end) @@ -255,7 +261,7 @@ defmodule DevRound.HostingTest do u3 = register_attendee(event, "u3", false, [lang2]) assert {:error, messages} = - Hosting.validate_team_generation_constraints([u1, u3], team_names) + Hosting.validate_team_generation_constraints([u1, u3], team_names, []) assert Enum.any?(messages, fn m -> m =~ "No team mate for u1" end) @@ -264,9 +270,26 @@ defmodule DevRound.HostingTest do # 3 checked in-person, needs 1 team. 1 remote (u2), not enough. # Total 4. Needs 2 teams. assert {:error, messages} = - Hosting.validate_team_generation_constraints([u1, u3, u4, u2], [hd(team_names)]) + Hosting.validate_team_generation_constraints( + [u1, u3, u4, u2], + [hd(team_names)], + [] + ) assert Enum.member?(messages, "Not enough team names for checked participants.") + + # 5. Not enough video conference rooms (needs 1 team for u2) + assert {:error, messages} = + Hosting.validate_team_generation_constraints( + [u2, register_attendee(event, "u5", true, [lang1])], + team_names, + [] + ) + + assert Enum.member?( + messages, + "Not enough session video conference room URLs to build teams for checked remote participants." + ) end end end