Skip to content

Commit 81d9d53

Browse files
committed
Improve
1 parent f1498d3 commit 81d9d53

15 files changed

Lines changed: 275 additions & 47 deletions

File tree

apps/codebattle/assets/js/widgets/middlewares/GroupTournament.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export const load = (groupTournamentId) => async (dispatch) => {
116116
"x-csrf-token": window.csrf_token,
117117
},
118118
});
119+
console.log(response);
119120

120121
dispatch(actions.setData(response));
121-
}
122+
};

apps/codebattle/assets/js/widgets/pages/groupTournament/EvolutionPanel.jsx

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,45 @@ import React from "react";
22

33
const getExternalUrl = (url) => `${url}/browse/README.md?rev=main&chatMessage=""`;
44

5+
const formatInsertedAt = (insertedAt) => {
6+
if (!insertedAt) {
7+
return null;
8+
}
9+
10+
const date = new Date(insertedAt);
11+
12+
if (Number.isNaN(date.getTime())) {
13+
return null;
14+
}
15+
16+
return date.toLocaleString();
17+
};
18+
19+
const buildRunSummary = (item, idx) => {
20+
if (!item || typeof item !== "object") {
21+
return {
22+
key: idx,
23+
title: `v${idx + 1}`,
24+
meta: item ? String(item) : null,
25+
};
26+
}
27+
28+
const playersCount = item.playerIds?.length;
29+
const meta = [
30+
item.status,
31+
playersCount ? `${playersCount} players` : null,
32+
formatInsertedAt(item.insertedAt),
33+
]
34+
.filter(Boolean)
35+
.join(" • ");
36+
37+
return {
38+
key: item.id ?? idx,
39+
title: `Run #${item.id ?? idx + 1}`,
40+
meta,
41+
};
42+
};
43+
544
function EvolutionPanel({ items, tournamentStatus, runId, setRunId, repoUrl }) {
645
return (
746
<div className="card cb-card border cb-border-color rounded h-100">
@@ -18,17 +57,32 @@ function EvolutionPanel({ items, tournamentStatus, runId, setRunId, repoUrl }) {
1857
{items && items.length > 0 && (
1958
<div className="mt-2 small">
2059
<div className="list-group list-group-flush">
21-
{items.map((item, idx) => (
22-
<button
23-
key={idx}
24-
type="button"
25-
onClick={() => setRunId(item.id)}
26-
className="list-group-item list-group-item-action px-0 py-1 border-0 text-left bg-transparent"
27-
>
28-
<span className="badge badge-secondary mr-2">v{idx + 1}</span>
29-
<span className="text-truncate">{item}</span>
30-
</button>
31-
))}
60+
{items.map((item, idx) => {
61+
const summary = buildRunSummary(item, idx);
62+
63+
return (
64+
<button
65+
key={summary.key}
66+
type="button"
67+
onClick={() => setRunId(item?.id)}
68+
className="list-group-item list-group-item-action px-0 py-1 border-0 text-left bg-transparent"
69+
style={{
70+
backgroundColor:
71+
runId === item?.id ? "rgba(40, 167, 69, 0.14)" : "transparent",
72+
}}
73+
>
74+
<div className="d-flex align-items-center">
75+
<span className="badge badge-secondary mr-2">v{idx + 1}</span>
76+
<span className="text-truncate">{summary.title}</span>
77+
</div>
78+
{summary.meta ? (
79+
<small className="d-block text-muted text-truncate mt-1">
80+
{summary.meta}
81+
</small>
82+
) : null}
83+
</button>
84+
);
85+
})}
3286
</div>
3387
</div>
3488
)}

apps/codebattle/assets/js/widgets/pages/groupTournament/GroupTournamentPage.jsx

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from "react";
1+
import React, { useEffect, useMemo, useState } from "react";
22
import { useDispatch, useSelector } from "react-redux";
33

44
import Header from "./Header";
@@ -11,6 +11,47 @@ import * as selectors from "../../selectors";
1111
import Loading from "@/components/Loading";
1212
import { load, requestInviteUpdate } from "@/middlewares/GroupTournament";
1313

14+
const getDateTimestamp = (value) => {
15+
if (!value) {
16+
return null;
17+
}
18+
19+
const timestamp = new Date(value).getTime();
20+
21+
return Number.isNaN(timestamp) ? null : timestamp;
22+
};
23+
24+
const findSolutionForRun = (run, solutionHistory) => {
25+
if (!run || !solutionHistory?.length) {
26+
return null;
27+
}
28+
29+
const solutionWithSameId = solutionHistory.find((solution) => solution.id === run.id);
30+
31+
if (solutionWithSameId) {
32+
return solutionWithSameId;
33+
}
34+
35+
const runInsertedAtTimestamp = getDateTimestamp(run.insertedAt);
36+
37+
if (runInsertedAtTimestamp === null) {
38+
return solutionHistory[0] || null;
39+
}
40+
41+
return (
42+
solutionHistory.find((solution) => {
43+
const solutionInsertedAtTimestamp = getDateTimestamp(solution.insertedAt);
44+
45+
return (
46+
solutionInsertedAtTimestamp !== null &&
47+
solutionInsertedAtTimestamp <= runInsertedAtTimestamp
48+
);
49+
}) ||
50+
solutionHistory[solutionHistory.length - 1] ||
51+
null
52+
);
53+
};
54+
1455
function GroupTournamentPage({ tournamentId, tournamentName, tournamentDescription }) {
1556
const dispatch = useDispatch();
1657

@@ -35,6 +76,14 @@ function GroupTournamentPage({ tournamentId, tournamentName, tournamentDescripti
3576
data,
3677
} = useSelector(selectors.groupTournamentSelector);
3778

79+
const solutionHistory = useMemo(() => data?.solutionHistory || [], [data?.solutionHistory]);
80+
const selectedRunSolution = useMemo(
81+
() => findSolutionForRun(selectedRun, solutionHistory),
82+
[selectedRun, solutionHistory],
83+
);
84+
const editorText = selectedRunSolution?.solution || code;
85+
const editorLang = selectedRunSolution?.lang || langSlug;
86+
3887
const openExternalRegistrationWindow = () => {
3988
setShowInviteWindow(true);
4089
};
@@ -60,7 +109,7 @@ function GroupTournamentPage({ tournamentId, tournamentName, tournamentDescripti
60109
if (tournamentId) {
61110
load(tournamentId)(dispatch);
62111
}
63-
}, [tournamentId, dispatch])
112+
}, [tournamentId, dispatch]);
64113

65114
if (invite.state === "loading" && requireInvitation) {
66115
return <Loading />;
@@ -124,7 +173,7 @@ function GroupTournamentPage({ tournamentId, tournamentName, tournamentDescripti
124173
/>
125174
</div>
126175
<div className="col-lg-4 col-md-4 col-12 p-1 pb-4">
127-
<EditorPanel text={code} lang={langSlug} />
176+
<EditorPanel text={editorText} lang={editorLang} />
128177
<LogPanel logs={logs} />
129178
</div>
130179
</div>

apps/codebattle/lib/codebattle/external_platform.ex

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -200,22 +200,45 @@ defmodule Codebattle.ExternalPlatform do
200200
def get_invite(_), do: {:error, :invalid_invite_id}
201201

202202
@doc """
203-
Forks a repository into the target organization.
204-
POST /repos/{org_slug}/{repo_slug}/fork
203+
Creates a repository in the target organization from a template repository.
204+
POST /orgs/{org_slug}/repos
205205
"""
206-
@spec fork_repo(String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
207-
def fork_repo(repo_slug, target_org_slug, opts \\ []) do
208-
source_org_slug = Keyword.get(opts, :source_org_slug, default_org_slug())
206+
@spec create_repo_from_template(String.t(), keyword()) :: {:ok, map()} | {:error, term()}
207+
def create_repo_from_template(target_org_slug, opts \\ []) do
208+
slug = opts[:slug]
209+
template_id = opts[:template_id]
209210

210-
body = %{org_slug: target_org_slug}
211-
body = if opts[:slug], do: Map.put(body, :slug, opts[:slug]), else: body
211+
cond do
212+
!is_binary(target_org_slug) || String.trim(target_org_slug) == "" ->
213+
{:error, :invalid_org_slug}
212214

213-
body =
214-
if Keyword.has_key?(opts, :default_branch_only),
215-
do: Map.put(body, :default_branch_only, opts[:default_branch_only]),
216-
else: body
215+
!is_binary(slug) || String.trim(slug) == "" ->
216+
{:error, :invalid_repo_slug}
217+
218+
!is_binary(template_id) || String.trim(template_id) == "" ->
219+
{:error, :invalid_template_id}
220+
221+
true ->
222+
do_create_repo_from_template(String.trim(target_org_slug), opts)
223+
end
224+
end
217225

218-
url = "#{external_platform_service_url()}/repos/#{source_org_slug}/#{repo_slug}/fork"
226+
defp do_create_repo_from_template(target_org_slug, opts) do
227+
slug = String.trim(opts[:slug])
228+
template_id = String.trim(opts[:template_id])
229+
230+
body =
231+
%{
232+
name: Keyword.get(opts, :name, slug),
233+
slug: slug,
234+
description: opts[:description],
235+
visibility: Keyword.get(opts, :visibility, "public"),
236+
templating_options: %{template_id: template_id}
237+
}
238+
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
239+
|> Map.new()
240+
241+
url = "#{external_platform_service_url()}/orgs/#{target_org_slug}/repos"
219242

220243
req_opts =
221244
Keyword.merge(
@@ -225,7 +248,7 @@ defmodule Codebattle.ExternalPlatform do
225248
receive_timeout: @invite_timeout_ms
226249
)
227250

228-
Logger.info("ExternalPlatform.fork_repo START method=POST url=#{url} body=#{inspect(body)}")
251+
Logger.info("ExternalPlatform.create_repo_from_template START method=POST url=#{url} body=#{inspect(body)}")
229252

230253
started_at = System.monotonic_time(:millisecond)
231254
result = safe_request(:post, url, req_opts)
@@ -234,20 +257,22 @@ defmodule Codebattle.ExternalPlatform do
234257
case result do
235258
{:ok, %{status: status, body: resp_body}} when status in [200, 201] ->
236259
Logger.info(
237-
"ExternalPlatform.fork_repo OK url=#{url} status=#{status} duration_ms=#{duration_ms} body=#{inspect(resp_body)}"
260+
"ExternalPlatform.create_repo_from_template OK url=#{url} status=#{status} duration_ms=#{duration_ms} body=#{inspect(resp_body)}"
238261
)
239262

240263
{:ok, resp_body}
241264

242265
{:ok, %{status: status, body: resp_body}} ->
243266
Logger.warning(
244-
"ExternalPlatform.fork_repo FAIL url=#{url} status=#{status} duration_ms=#{duration_ms} body=#{inspect(resp_body)}"
267+
"ExternalPlatform.create_repo_from_template FAIL url=#{url} status=#{status} duration_ms=#{duration_ms} body=#{inspect(resp_body)}"
245268
)
246269

247270
{:error, resp_body}
248271

249272
{:error, reason} ->
250-
Logger.warning("ExternalPlatform.fork_repo ERROR url=#{url} duration_ms=#{duration_ms} reason=#{inspect(reason)}")
273+
Logger.warning(
274+
"ExternalPlatform.create_repo_from_template ERROR url=#{url} duration_ms=#{duration_ms} reason=#{inspect(reason)}"
275+
)
251276

252277
{:error, reason}
253278
end

apps/codebattle/lib/codebattle/group_tournament.ex

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ defmodule Codebattle.GroupTournament do
3333
:last_round_ended_at,
3434
:meta,
3535
:require_invitation,
36-
:run_on_external_platform
36+
:run_on_external_platform,
37+
:template_id
3738
]}
3839

3940
schema "group_tournaments" do
@@ -53,6 +54,7 @@ defmodule Codebattle.GroupTournament do
5354
field(:include_bots, :boolean, default: false)
5455
field(:require_invitation, :boolean, default: false)
5556
field(:run_on_external_platform, :boolean, default: false)
57+
field(:template_id, :string)
5658
field(:last_round_started_at, :naive_datetime)
5759
field(:last_round_ended_at, :naive_datetime)
5860
field(:meta, :map, default: %{})
@@ -84,6 +86,7 @@ defmodule Codebattle.GroupTournament do
8486
:include_bots,
8587
:require_invitation,
8688
:run_on_external_platform,
89+
:template_id,
8790
:last_round_started_at,
8891
:last_round_ended_at,
8992
:meta
@@ -99,12 +102,15 @@ defmodule Codebattle.GroupTournament do
99102
:round_timeout_seconds
100103
])
101104
|> update_change(:slug, &normalize_slug/1)
105+
|> update_change(:template_id, &normalize_optional_string/1)
102106
|> validate_inclusion(:state, @states)
103107
|> validate_length(:name, min: 2, max: 255)
104108
|> validate_length(:slug, min: 2, max: 255)
105109
|> validate_length(:description, min: 3, max: 7531)
110+
|> validate_length(:template_id, max: 255)
106111
|> validate_number(:rounds_count, greater_than: 0)
107112
|> validate_number(:round_timeout_seconds, greater_than: 0)
113+
|> validate_template_id()
108114
|> foreign_key_constraint(:creator_id)
109115
|> foreign_key_constraint(:group_task_id)
110116
end
@@ -113,4 +119,23 @@ defmodule Codebattle.GroupTournament do
113119

114120
defp normalize_slug(nil), do: nil
115121
defp normalize_slug(slug), do: slug |> String.trim() |> String.downcase()
122+
123+
defp normalize_optional_string(nil), do: nil
124+
125+
defp normalize_optional_string(value) when is_binary(value) do
126+
case String.trim(value) do
127+
"" -> nil
128+
trimmed -> trimmed
129+
end
130+
end
131+
132+
defp normalize_optional_string(value), do: value
133+
134+
defp validate_template_id(changeset) do
135+
if get_field(changeset, :run_on_external_platform) do
136+
validate_required(changeset, [:template_id])
137+
else
138+
changeset
139+
end
140+
end
116141
end

apps/codebattle/lib/codebattle/tournament/strategy/base.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1400,7 +1400,8 @@ defmodule Codebattle.Tournament.Base do
14001400
starts_at: DateTime.utc_now(),
14011401
rounds_count: meta[:rounds_count] || 1,
14021402
round_timeout_seconds: meta[:round_timeout_seconds] || 3600,
1403-
run_on_external_platform: meta[:run_on_external_platform] || false
1403+
run_on_external_platform: meta[:run_on_external_platform] || false,
1404+
template_id: meta[:template_id]
14041405
}
14051406

14061407
case Codebattle.GroupTournament.Context.create_group_tournament(attrs) do

apps/codebattle/lib/codebattle/user_group_tournament/context.ex

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,9 @@ defmodule Codebattle.UserGroupTournament.Context do
173173
defp ensure_repo(%UserGroupTournament{repo_state: "completed"} = record, _group_tournament, _user), do: {:ok, record}
174174

175175
defp ensure_repo(%UserGroupTournament{} = record, %GroupTournament{} = group_tournament, %User{} = user) do
176-
case ExternalPlatform.fork_repo(group_tournament.slug, target_org_slug(), slug: repo_slug_for(user, group_tournament)) do
176+
repo_slug = repo_slug_for(user, group_tournament)
177+
178+
case create_repo_for_tournament(group_tournament, repo_slug) do
177179
{:ok, response} ->
178180
{:ok,
179181
update!(record, %{
@@ -301,6 +303,19 @@ defmodule Codebattle.UserGroupTournament.Context do
301303
defp repo_slug(group_tournament_slug, ""), do: group_tournament_slug
302304
defp repo_slug(group_tournament_slug, login), do: "#{group_tournament_slug}-#{login}"
303305

306+
defp create_repo_for_tournament(%GroupTournament{template_id: template_id} = group_tournament, repo_slug)
307+
when is_binary(template_id) and template_id != "" do
308+
ExternalPlatform.create_repo_from_template(
309+
target_org_slug(),
310+
name: repo_slug,
311+
slug: repo_slug,
312+
description: group_tournament.description,
313+
template_id: template_id
314+
)
315+
end
316+
317+
defp create_repo_for_tournament(_group_tournament, _repo_slug), do: {:error, %{error: "template_id is required"}}
318+
304319
defp ensure_platform_identity(%User{} = user) do
305320
lookup_login = platform_identity_lookup_login(user)
306321

@@ -352,6 +367,8 @@ defmodule Codebattle.UserGroupTournament.Context do
352367
|> Repo.update!()
353368
end
354369

370+
defp extract_repo_url(%{"web_url" => repo_url}) when is_binary(repo_url) and repo_url != "", do: repo_url
371+
defp extract_repo_url(%{web_url: repo_url}) when is_binary(repo_url) and repo_url != "", do: repo_url
355372
defp extract_repo_url(%{"repo_url" => repo_url}) when is_binary(repo_url) and repo_url != "", do: repo_url
356373
defp extract_repo_url(%{repo_url: repo_url}) when is_binary(repo_url) and repo_url != "", do: repo_url
357374
defp extract_repo_url(%{"url" => repo_url}) when is_binary(repo_url) and repo_url != "", do: repo_url

0 commit comments

Comments
 (0)