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