Skip to content

Commit ad5872a

Browse files
committed
add moderators
1 parent 20c9604 commit ad5872a

14 files changed

Lines changed: 283 additions & 12 deletions

File tree

apps/codebattle/assets/js/widgets/pages/tournament/EditTournament.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ function EditTournament({ tournamentId, taskPackNames = [], userTimezone = "UTC"
183183
const initialValues = {
184184
name: tournament.name || "",
185185
description: tournament.description || "",
186+
moderator_ids: (tournament.moderatorIds || []).join(", "),
186187
starts_at: formatDatetimeLocal(tournament.startsAt),
187188
access_type: tournament.accessType || "public",
188189
task_provider: tournament.taskProvider || "level",

apps/codebattle/assets/js/widgets/pages/tournament/Tournament.jsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,17 @@ function Tournament() {
134134
() => [TournamentStates.finished, TournamentStates.canceled].includes(tournament.state),
135135
[tournament.state],
136136
);
137-
const canModerate = useMemo(() => isAdmin, [isAdmin]);
137+
const canModerate = useMemo(() => {
138+
if (!currentUserId) {
139+
return false;
140+
}
141+
142+
return (
143+
isAdmin ||
144+
tournament.creatorId === currentUserId ||
145+
(tournament.moderatorIds || []).includes(currentUserId)
146+
);
147+
}, [currentUserId, isAdmin, tournament.creatorId, tournament.moderatorIds]);
138148
const hiddenSidePanel =
139149
streamMode ||
140150
(tournament.state === TournamentStates.finished && !tournament.useChat && !tournament.useClan);
@@ -166,7 +176,7 @@ function Tournament() {
166176
useEffect(() => {
167177
const tournamentChannel = dispatch(connectToTournament(tournament?.id));
168178

169-
if (isAdmin) {
179+
if (canModerate) {
170180
const tournamentAdminChannel = dispatch(connectToTournamentAdmin(tournament?.id, true));
171181

172182
return () => {
@@ -179,7 +189,7 @@ function Tournament() {
179189
tournamentChannel.leave();
180190
};
181191
// eslint-disable-next-line react-hooks/exhaustive-deps
182-
}, [isAdmin]);
192+
}, [canModerate]);
183193

184194
useEffect(() => {
185195
if (tournament.isLive) {

apps/codebattle/assets/js/widgets/pages/tournament/TournamentForm.jsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function TournamentForm({
5959
const [formData, setFormData] = useState({
6060
name: initialValues.name || "",
6161
description: initialValues.description || "",
62+
moderator_ids: initialValues.moderator_ids || "",
6263
starts_at: initialValues.starts_at || "",
6364
access_type: initialValues.access_type || "public",
6465
task_provider: initialValues.task_provider || "level",
@@ -97,6 +98,10 @@ function TournamentForm({
9798
e.preventDefault();
9899

99100
const payload = { ...formData };
101+
payload.moderator_ids = formData.moderator_ids
102+
.split(/[\s,]+/)
103+
.map((id) => id.trim())
104+
.filter(Boolean);
100105
payload.round_timeout_seconds =
101106
formData.timeout_mode === "per_round" ? formData.round_timeout_seconds : null;
102107

@@ -168,6 +173,28 @@ function TournamentForm({
168173
/>
169174
{renderError("description")}
170175
</div>
176+
177+
<div className="form-group mb-0">
178+
<label htmlFor="moderator_ids" className="form-label text-white">
179+
Moderator IDs
180+
</label>
181+
<textarea
182+
id="moderator_ids"
183+
name="moderator_ids"
184+
className={cn("form-control cb-bg-panel cb-border-color text-white cb-rounded", {
185+
"is-invalid": errors.moderator_ids,
186+
})}
187+
value={formData.moderator_ids}
188+
onChange={handleChange}
189+
rows={3}
190+
placeholder="42, 1337"
191+
/>
192+
<div className="form-text text-muted">
193+
Enter user IDs separated by commas or spaces. The creator is always treated as a
194+
moderator and does not need to be listed here.
195+
</div>
196+
{renderError("moderator_ids")}
197+
</div>
171198
</div>
172199
</div>
173200

@@ -609,6 +636,7 @@ TournamentForm.propTypes = {
609636
initialValues: PropTypes.shape({
610637
name: PropTypes.string,
611638
description: PropTypes.string,
639+
moderator_ids: PropTypes.string,
612640
starts_at: PropTypes.string,
613641
access_type: PropTypes.string,
614642
task_provider: PropTypes.string,
@@ -633,6 +661,7 @@ TournamentForm.propTypes = {
633661
base: PropTypes.string,
634662
name: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
635663
description: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
664+
moderator_ids: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
636665
starts_at: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
637666
access_type: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
638667
task_provider: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),

apps/codebattle/lib/codebattle/tournament/context.ex

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ defmodule Codebattle.Tournament.Context do
191191
where:
192192
t.starts_at >= ^datetime_from and
193193
t.starts_at <= ^datetime_to and
194-
(t.creator_id == ^user_id or fragment("? = ANY(?)", ^user_id, t.winner_ids))
194+
(t.creator_id == ^user_id or
195+
fragment("? = ANY(?)", ^user_id, t.moderator_ids) or
196+
fragment("? = ANY(?)", ^user_id, t.winner_ids))
195197
)
196198
)
197199
end
@@ -402,6 +404,7 @@ defmodule Codebattle.Tournament.Context do
402404
|> Map.delete("creator")
403405
|> AtomizedMap.atomize()
404406
|> Map.put(:creator, params["creator"] || %{})
407+
|> normalize_moderator_ids()
405408

406409
timeout_mode =
407410
params[:timeout_mode] ||
@@ -447,6 +450,38 @@ defmodule Codebattle.Tournament.Context do
447450
})
448451
end
449452

453+
defp normalize_moderator_ids(%{moderator_ids: moderator_ids} = params) do
454+
creator_id = get_in(params, [:creator, :id])
455+
456+
normalized_moderator_ids =
457+
moderator_ids
458+
|> List.wrap()
459+
|> Enum.map(fn
460+
id when is_integer(id) -> id
461+
id when is_binary(id) -> String.trim(id)
462+
_ -> nil
463+
end)
464+
|> Enum.reject(&is_nil/1)
465+
|> Enum.reject(&(&1 == ""))
466+
|> Enum.map(fn
467+
id when is_integer(id) ->
468+
id
469+
470+
id when is_binary(id) ->
471+
case Integer.parse(id) do
472+
{parsed_id, ""} -> parsed_id
473+
_ -> nil
474+
end
475+
end)
476+
|> Enum.reject(&is_nil/1)
477+
|> Enum.reject(&(&1 == creator_id))
478+
|> Enum.uniq()
479+
480+
Map.put(params, :moderator_ids, normalized_moderator_ids)
481+
end
482+
483+
defp normalize_moderator_ids(params), do: Map.put(params, :moderator_ids, [])
484+
450485
def get_tournament_for_restore do
451486
@states_from_restore
452487
|> get_db_tournaments()

apps/codebattle/lib/codebattle/tournament/helpers.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,10 @@ defmodule Codebattle.Tournament.Helpers do
141141

142142
def can_be_started?(_t), do: false
143143

144+
def can_moderate?(_tournament, nil), do: false
145+
144146
def can_moderate?(tournament, user) do
145-
creator?(tournament, user) || User.admin?(user)
147+
creator?(tournament, user) || moderator?(tournament, user) || User.admin?(user)
146148
end
147149

148150
def can_access?(%{access_type: "token"} = tournament, user, params) do
@@ -179,10 +181,18 @@ defmodule Codebattle.Tournament.Helpers do
179181
end
180182
end
181183

184+
def creator?(_tournament, nil), do: false
185+
182186
def creator?(tournament, user) do
183187
tournament.creator_id == user.id
184188
end
185189

190+
def moderator?(_tournament, nil), do: false
191+
192+
def moderator?(tournament, user) do
193+
user.id in (tournament.moderator_ids || [])
194+
end
195+
186196
def calc_round_result(round_position) do
187197
round_position
188198
|> Enum.map(&calc_match_result/1)

apps/codebattle/lib/codebattle/tournament/tournament.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ defmodule Codebattle.Tournament do
3232
:match_timeout_seconds,
3333
:matches,
3434
:meta,
35+
:moderator_ids,
3536
:name,
3637
:players,
3738
:players_count,
@@ -94,6 +95,7 @@ defmodule Codebattle.Tournament do
9495
field(:match_timeout_seconds, :integer, default: @default_match_timeout)
9596
field(:matches, AtomizedMap, default: %{})
9697
field(:meta, AtomizedMap, default: %{})
98+
field(:moderator_ids, {:array, :integer}, default: [])
9799
field(:name, :string)
98100
field(:players, AtomizedMap, default: %{})
99101
field(:players_limit, :integer)
@@ -158,6 +160,7 @@ defmodule Codebattle.Tournament do
158160
:match_timeout_seconds,
159161
:matches,
160162
:meta,
163+
:moderator_ids,
161164
:name,
162165
:played_pair_ids,
163166
:players,

apps/codebattle/lib/codebattle_web/channels/tournament_admin_channel.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ defmodule CodebattleWeb.TournamentAdminChannel do
4444

4545
with tournament when not is_nil(tournament) <-
4646
Tournament.Context.get!(tournament_id),
47-
true <- Codebattle.User.admin?(current_user) do
47+
true <- Helpers.can_moderate?(tournament, current_user) do
4848
Codebattle.PubSub.subscribe("tournament:#{tournament.id}")
4949
Codebattle.PubSub.subscribe("tournament:#{tournament.id}:common")
5050

apps/codebattle/lib/codebattle_web/controllers/api/v1/group_tournament_controller.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ defmodule CodebattleWeb.Api.V1.GroupTournamentController do
163163
end
164164
end
165165

166+
defp parse_user_id(_user_id), do: :error
167+
166168
defp create_token_for_user(conn, group_tournament, user_id) do
167169
case parse_user_id(user_id) do
168170
{:ok, parsed_user_id} ->
@@ -186,6 +188,4 @@ defmodule CodebattleWeb.Api.V1.GroupTournamentController do
186188
|> json(%{errors: translate_errors(changeset)})
187189
end
188190
end
189-
190-
defp parse_user_id(_user_id), do: :error
191191
end

apps/codebattle/lib/codebattle_web/controllers/api/v1/tournament_controller.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule CodebattleWeb.Api.V1.TournamentController do
22
use CodebattleWeb, :controller
33

44
alias Codebattle.Tournament
5-
alias Codebattle.User
5+
alias Codebattle.Tournament.Helpers
66

77
def index(conn, params) do
88
current_user = conn.assigns.current_user
@@ -54,7 +54,7 @@ defmodule CodebattleWeb.Api.V1.TournamentController do
5454
tournament = Tournament.Context.get!(id)
5555

5656
# Check if user has permission to update
57-
if tournament.creator_id == current_user.id || User.admin?(current_user) do
57+
if Helpers.can_moderate?(tournament, current_user) do
5858
params =
5959
Map.put(
6060
tournament_params,

apps/codebattle/lib/codebattle_web/controllers/tournament_controller.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ defmodule CodebattleWeb.TournamentController do
44
import PhoenixGon.Controller
55

66
alias Codebattle.Tournament
7+
alias Codebattle.Tournament.Helpers
78
alias Codebattle.User
89

910
plug(CodebattleWeb.Plugs.RequireAuth when action in [:index, :show, :edit])
@@ -24,7 +25,7 @@ defmodule CodebattleWeb.TournamentController do
2425
current_user = conn.assigns[:current_user]
2526
tournament = Tournament.Context.get!(params["id"])
2627

27-
if Tournament.Helpers.can_access?(tournament, current_user, params) do
28+
if Helpers.can_access?(tournament, current_user, params) do
2829
handle_tournament_for_user(conn, tournament, current_user, params)
2930
else
3031
conn
@@ -39,7 +40,7 @@ defmodule CodebattleWeb.TournamentController do
3940
tournament = Tournament.Context.get!(id)
4041

4142
# Check if user has permission to edit
42-
if tournament.creator_id == current_user.id || User.admin?(current_user) do
43+
if Helpers.can_moderate?(tournament, current_user) do
4344
user_timezone = get_in(conn.private, [:connect_params, "timezone"]) || "UTC"
4445

4546
task_pack_names =

0 commit comments

Comments
 (0)