Repository: omegaup/omegaup
Pull request #8147: [FEATURE] Add "Recommended" checkbox in create and update contest forms
Author: login: Avadhutgiri
Created: 2025-04-04T11:56:10Z | Merged: 2025-04-27T04:29:48Z

## Summary

This pull request implements the "Recommended" checkbox feature for contests, covering both contest creation and contest editing workflows.

Fixes: #7957   
**[FEATURE] Add checkbox to edit contest UI to mark them as recommended**

---

## Feature Overview

Previously, the `recommended` property existed in the backend, but there was no way to set or view it through the UI.  
This PR introduces the complete end-to-end functionality:

- Adds a "Recommended" checkbox to the Create Contest form
- Adds the same checkbox to the Edit Contest form
- Ensures the state is correctly:
  - Initialized from backend data
  - Passed through form submission
  - Persisted in the database
  - Displayed correctly when re-editing a contest

---

## Implementation Details

### 1. Frontend

- **Form Changes** (`NewForm.vue`):
  - Added a new `recommended` checkbox field in both create and edit modes
  - The field is initialized from `initialRecommended` in props (edit mode)
  - The value is included in the `contest` object during submission

- **Event Handling**:
  - The `recommended` flag is included in emitted form events and merged into the `details` object after submission

### 2. Backend

- **`apiUpdate` Method**:
  - Added `recommended` to the `valueProperties` array to allow updates

- **`getCachedDetails` Method**:
  - Included `'recommended'` in the `asFilteredArray` return to ensure the frontend receives the correct value during edit

---

## Result


With this PR:
- The "Recommended" checkbox is now visible and functional during both contest creation and editing
- The selected value is saved, retrieved, and persisted
- The feature completes the frontend-backend loop for the `recommended` property

---

## Key Insight

This feature required updates to both the form logic and the backend data flow:
- In `apiUpdate` to allow the property to be saved
- In `getCachedDetails` to send the value back to the frontend

This ensures the UI and backend remain in sync throughout the contest lifecycle.

I have added an attachment from administrative perpective as well as user perspective





https://github.com/user-attachments/assets/27eeffca-9e5b-4f8c-929a-0fc39a597735





Diff:
diff --git a/frontend/server/src/Controllers/Contest.php b/frontend/server/src/Controllers/Contest.php
index fbdb1344169..2c400ae30f3 100644
--- a/frontend/server/src/Controllers/Contest.php
+++ b/frontend/server/src/Controllers/Contest.php
@@ -22,9 +22,9 @@
  * @psalm-type ProblemVersion=array{author: Signature, commit: string, committer: Signature, message: string, parents: list<string>, tree: array<string, string>, version: string}
  * @psalm-type ProblemsetProblemWithVersions=array{accepted: int, accepts_submissions: bool, alias: string, commit: string, difficulty: float, has_submissions: bool, input_limit: int, languages: string, letter?: string, order: int, points: float, quality_payload?: ProblemQualityPayload, quality_seal: bool, submissions: int, title: string, version: string, versions: array{log: list<ProblemVersion>, published: string}, visibility: int, visits: int}
  * @psalm-type ContestListMinePayload=array{contests: list<Contest>, privateContestsAlert: bool}
- * @psalm-type ContestDetails=array{admin: bool, admission_mode: string, alias: string, archived: bool, contest_for_teams: bool, description: string, director: string, feedback: string, finish_time: \OmegaUp\Timestamp, has_submissions: bool, languages: list<string>, needs_basic_information: bool, opened: bool, original_contest_alias: null|string, original_problemset_id: int|null, penalty: int, penalty_calc_policy: string, penalty_type: string, points_decay_factor: float, problems: list<ProblemsetProblem>, problemset_id: int, requests_user_information: string, rerun_id?: int, score_mode: string, scoreboard: int, scoreboard_url?: string, scoreboard_url_admin?: string, show_penalty: bool, default_show_all_contestants_in_scoreboard: bool, show_scoreboard_after: bool, start_time: \OmegaUp\Timestamp, submission_deadline?: \OmegaUp\Timestamp|null, submissions_gap: int, title: string, window_length: int|null}
+ * @psalm-type ContestDetails=array{admin: bool, admission_mode: string, alias: string, archived: bool, contest_for_teams: bool, description: string, director: string, feedback: string, finish_time: \OmegaUp\Timestamp, has_submissions: bool, languages: list<string>, needs_basic_information: bool, opened: bool, original_contest_alias: null|string, original_problemset_id: int|null, penalty: int, penalty_calc_policy: string, penalty_type: string, points_decay_factor: float, problems: list<ProblemsetProblem>, problemset_id: int, recommended: bool, requests_user_information: string, rerun_id?: int, score_mode: string, scoreboard: int, scoreboard_url?: string, scoreboard_url_admin?: string, show_penalty: bool, default_show_all_contestants_in_scoreboard: bool, show_scoreboard_after: bool, start_time: \OmegaUp\Timestamp, submission_deadline?: \OmegaUp\Timestamp|null, submissions_gap: int, title: string, window_length: int|null}
  * @psalm-type ArenaContest=array{alias: string, director: string, finish_time: \OmegaUp\Timestamp|null, start_time: \OmegaUp\Timestamp|null, rerun_id?: int, title: string, window_length?: int}
- * @psalm-type ContestAdminDetails=array{admin: bool, admission_mode: string, alias: string, archived: bool, available_languages: array<string, string>, contest_for_teams: bool, description: string, director: string, feedback: string, finish_time: \OmegaUp\Timestamp, has_submissions: bool, languages: list<string>, needs_basic_information: bool, opened: bool, original_contest_alias: null|string, original_problemset_id: int|null, penalty: int, penalty_calc_policy: string, penalty_type: string, points_decay_factor: float, problems?: list<ProblemsetProblem>, problemset_id: int, requests_user_information: string, rerun_id?: int, score_mode: string, scoreboard: int, scoreboard_url: null|string, scoreboard_url_admin: null|string, show_penalty: bool, default_show_all_contestants_in_scoreboard: bool, show_scoreboard_after: bool, start_time: \OmegaUp\Timestamp, submission_deadline?: \OmegaUp\Timestamp|null, submissions_gap: int, title: string, window_length: int|null}
+ * @psalm-type ContestAdminDetails=array{admin: bool, admission_mode: string, alias: string, archived: bool, available_languages: array<string, string>, canSetRecommended: bool, contest_for_teams: bool, description: string, director: string, feedback: string, finish_time: \OmegaUp\Timestamp, has_submissions: bool, languages: list<string>, needs_basic_information: bool, opened: bool, original_contest_alias: null|string, original_problemset_id: int|null, penalty: int, penalty_calc_policy: string, penalty_type: string, points_decay_factor: float, problems?: list<ProblemsetProblem>, problemset_id: int, recommended?: bool, requests_user_information: string, rerun_id?: int, score_mode: string, scoreboard: int, scoreboard_url: null|string, scoreboard_url_admin: null|string, show_penalty: bool, default_show_all_contestants_in_scoreboard: bool, show_scoreboard_after: bool, start_time: \OmegaUp\Timestamp, submission_deadline?: \OmegaUp\Timestamp|null, submissions_gap: int, title: string, window_length: int|null}
  * @psalm-type StatsPayload=array{alias: string, entity_type: string, cases_stats?: array<string, int>, pending_runs: list<string>, total_runs: int, verdict_counts: array<string, int>, max_wait_time?: \OmegaUp\Timestamp|null, max_wait_time_guid?: null|string, distribution?: array<int, int>, size_of_bucket?: float, total_points?: float}
  * @psalm-type ContestPublicDetails=array{admission_mode: string, alias: string, description: string, director: string, extra_note?: string|null, feedback: string, finish_time: \OmegaUp\Timestamp, languages: string, penalty: int, penalty_calc_policy: string, penalty_type: string, points_decay_factor: float, problemset_id: int, rerun_id: int|null, score_mode: string, scoreboard: int, show_penalty: bool, default_show_all_contestants_in_scoreboard: bool, show_scoreboard_after: bool, start_time: \OmegaUp\Timestamp, submissions_gap: int, title: string, user_registration_requested?: bool, user_registration_answered?: bool, user_registration_accepted?: bool|null, window_length: int|null}
  * @psalm-type ContestVirtualDetailsPayload=array{contest: ContestPublicDetails}
@@ -37,7 +37,7 @@
  * @psalm-type TimeTypeContests=array<string, list<ContestListItem>>
  * @psalm-type ContestListPayload=array{contests: list<ContestListItem>, countContests: int, query: string | null}
  * @psalm-type ContestListv2Payload=array{contests: ContestList, countContests: array{current: int, future: int, past: int}, pageSize: int, query: string | null}
- * @psalm-type ContestNewPayload=array{languages: array<string, string>, hasVisitedSection?: bool}
+ * @psalm-type ContestNewPayload=array{languages: array<string, string>, hasVisitedSection?: bool, canSetRecommended?: bool}
  * @psalm-type Run=array{alias: string, classname: string, contest_alias: null|string, contest_score: float|null, country: string, execution: null|string, guid: string, language: string, memory: int, output: null|string, penalty: int, runtime: int, score: float, score_by_group?: array<string, float|null>, status: string, status_memory: null|string, status_runtime: null|string, submit_delay: int, suggestions?: int, time: \OmegaUp\Timestamp, type: null|string, username: string, verdict: string}
  * @psalm-type RunMetadata=array{verdict: string, time: float, sys_time: int, wall_time: float, memory: int}
  * @psalm-type ScoreboardEvent=array{classname: string, country: string, delta: float, is_invited: bool, total: array{points: float, penalty: float}, name: null|string, username: string, problem: array{alias: string, points: float, penalty: float}}
@@ -75,6 +75,7 @@
  * @psalm-type ProblemDetails=array{accepts_submissions: bool, accepted: int, admin?: bool, alias: string, allow_user_add_tags: bool, commit: string, creation_date: \OmegaUp\Timestamp, difficulty: float|null, email_clarifications: bool, input_limit: int, karel_problem: bool, languages: list<string>, letter?: string, limits: SettingLimits, nextExecutionTimestamp?: \OmegaUp\Timestamp, nextSubmissionTimestamp?: \OmegaUp\Timestamp, nominationStatus: NominationStatus, order: string, points: float, preferred_language?: string, problem_id: int, problemsetter?: ProblemsetterInfo, quality_seal: bool, runs?: list<RunWithDetails>, score: float, settings: ProblemSettingsDistrib, show_diff: string, solvers?: list<BestSolvers>, source?: string, statement: ProblemStatement, submissions: int, title: string, version: string, visibility: int, visits: int}
  * @psalm-type ContestPrintDetailsPayload=array{contestTitle: string, problems: array<int, null|ProblemDetails>}
  */
+
 class Contest extends \OmegaUp\Controllers\Controller {
     const SHOW_INTRO = true;
     const MAX_CONTEST_LENGTH_SECONDS = 2678400; // 31 days
@@ -1379,6 +1380,9 @@ public static function getContestNewForTypeScript(
                     'hasVisitedSection' => \OmegaUp\UITools::hasVisitedSection(
                         'has-visited-create-contest'
                     ),
+                    'canSetRecommended' => \OmegaUp\Authorization::isSupportTeamMember(
+                        $r->identity
+                    ),
                 ],
                 'title' => new \OmegaUp\TranslationString(
                     'omegaupTitleContestNew'
@@ -1884,7 +1888,7 @@ private static function getCachedDetails(
             /** @return ContestDetails */
             function () use ($contest) {
                 // Initialize response to be the contest information
-                /** @var array{admission_mode: string, alias: string, archived: bool, contest_for_teams: bool, description: string, feedback: string, finish_time: \OmegaUp\Timestamp, has_submissions: bool, languages: string, score_mode: string, penalty: int, penalty_calc_policy: string, penalty_type: string, points_decay_factor: float, problemset_id: int, rerun_id: int|null, scoreboard: int, scoreboard_url: string, scoreboard_url_admin: string, default_show_all_contestants_in_scoreboard: bool, show_scoreboard_after: bool, start_time: \OmegaUp\Timestamp, submissions_gap: int, title: string, window_length: int|null} */
+                /** @var array{admission_mode: string, alias: string, archived: bool, contest_for_teams: bool, description: string, feedback: string, finish_time: \OmegaUp\Timestamp, has_submissions: bool, languages: string, score_mode: string, penalty: int, penalty_calc_policy: string, penalty_type: string, points_decay_factor: float, problemset_id: int, recommended: bool, rerun_id: int|null, scoreboard: int, scoreboard_url: string, scoreboard_url_admin: string, default_show_all_contestants_in_scoreboard: bool, show_scoreboard_after: bool, start_time: \OmegaUp\Timestamp, submissions_gap: int, title: string, window_length: int|null} */
                 $result = $contest->asFilteredArray([
                     'admission_mode',
                     'alias',
@@ -1901,6 +1905,7 @@ function () use ($contest) {
                     'penalty_type',
                     'points_decay_factor',
                     'problemset_id',
+                    'recommended',
                     'rerun_id',
                     'scoreboard',
                     'scoreboard_url',
@@ -2139,6 +2144,10 @@ public static function getContestDetailsForAdmin(
         $result['admin'] = true;
         $result['scoreboard_url'] = $problemset->scoreboard_url;
         $result['scoreboard_url_admin'] = $problemset->scoreboard_url_admin;
+        $result['recommended'] = $contest->recommended;
+        $result['canSetRecommended'] = \OmegaUp\Authorization::isSupportTeamMember(
+            $adminIdentity
+        );
         return $result;
     }
 
@@ -2629,6 +2638,8 @@ private static function createContest(
      * @omegaup-request-param null|string $teams_group_alias
      * @omegaup-request-param null|string $title
      * @omegaup-request-param int $window_length
+     * @omegaup-request-param bool|null $recommended
+
      */
     public static function apiCreate(\OmegaUp\Request $r) {
         \OmegaUp\Controllers\Controller::ensureNotInLockdown();
@@ -2671,6 +2682,9 @@ public static function apiCreate(\OmegaUp\Request $r) {
             ['partial','all_or_nothing','max_per_group'],
         );
         $checkPlagiarism = $r->ensureOptionalBool('check_plagiarism') ?? false;
+
+        // Handle recommended flag - only available for admins and support team
+        $recommendedValue = $r->ensureOptionalBool('recommended');
         $contest = new \OmegaUp\DAO\VO\Contests([
             'admission_mode' => 'private',
             'title' => $r['title'],
@@ -2693,6 +2707,16 @@ public static function apiCreate(\OmegaUp\Request $r) {
             'check_plagiarism' => $checkPlagiarism ? true : false,
         ]);
 
+        if (!is_null($recommendedValue)) {
+            if (\OmegaUp\Authorization::isSupportTeamMember($r->identity)) {
+                $contest->recommended = $recommendedValue;
+            } else {
+                throw new \OmegaUp\Exceptions\ForbiddenAccessException(
+                    'userNotAllowed'
+                );
+            }
+        }
+
         self::createContest(
             $problemset,
             $contest,
@@ -4794,6 +4818,7 @@ private static function validateContestCanBePublic(\OmegaUp\DAO\VO\Contests $con
      * @omegaup-request-param 'contest_start'|'none'|'problem_open'|'runtime'|null $penalty_type
      * @omegaup-request-param float|null $points_decay_factor
      * @omegaup-request-param null|string $problems
+     * @omegaup-request-param bool|null $recommended
      * @omegaup-request-param 'no'|'optional'|'required'|null $requests_user_information
      * @omegaup-request-param 'all_or_nothing'|'max_per_group'|'partial'|null $score_mode
      * @omegaup-request-param float|null $scoreboard
@@ -4855,6 +4880,19 @@ public static function apiUpdate(\OmegaUp\Request $r): array {
 
         $updateProblemset = true;
         $updateRequests = false;
+
+        // Handle recommended flag - only available for admins and support team
+        $recommendedValue = $r->ensureOptionalBool('recommended');
+        if (!is_null($recommendedValue)) {
+            if (\OmegaUp\Authorization::isSupportTeamMember($r->identity)) {
+                $contest->recommended = $recommendedValue;
+            } else {
+                throw new \OmegaUp\Exceptions\ForbiddenAccessException(
+                    'userNotAllowed'
+                );
+            }
+        }
+
         // Update contest DAO
         if (!is_null($r['admission_mode'])) {
             \OmegaUp\Validators::validateOptionalInEnum(
diff --git a/frontend/server/src/Controllers/README.md b/frontend/server/src/Controllers/README.md
index 310f1bf0b56..66259eb5707 100644
--- a/frontend/server/src/Controllers/README.md
+++ b/frontend/server/src/Controllers/README.md
@@ -843,6 +843,7 @@ Creates a new contest
 | `penalty_type`              | `'contest_start'\|'none'\|'problem_open'\|'runtime'\|null` |             |
 | `points_decay_factor`       | `float\|null`                                              |             |
 | `problems`                  | `null\|string`                                             |             |
+| `recommended`               | `bool\|null`                                               |             |
 | `requests_user_information` | `bool\|null`                                               |             |
 | `score_mode`                | `'all_or_nothing'\|'max_per_group'\|'partial'\|null`       |             |
 | `scoreboard`                | `float\|null`                                              |             |
@@ -1424,6 +1425,7 @@ Update a Contest
 | `penalty_type`                               | `'contest_start'\|'none'\|'problem_open'\|'runtime'\|null` |             |
 | `points_decay_factor`                        | `float\|null`                                              |             |
 | `problems`                                   | `null\|string`                                             |             |
+| `recommended`                                | `bool\|null`                                               |             |
 | `requests_user_information`                  | `'no'\|'optional'\|'required'\|null`                       |             |
 | `score_mode`                                 | `'all_or_nothing'\|'max_per_group'\|'partial'\|null`       |             |
 | `scoreboard`                                 | `float\|null`                                              |             |
diff --git a/frontend/templates/en.lang b/frontend/templates/en.lang
index 7424c85009a..75a75179d75 100644
--- a/frontend/templates/en.lang
+++ b/frontend/templates/en.lang
@@ -398,6 +398,9 @@ contestNewFormPenaltyType = "Penalty type"
 contestNewFormPenaltyTypeDesc = "Indicates how the submission penalty is calculated: minutes since the beginning of the contest, minutes since the problem was opened, or program execution time (milliseconds)."
 contestNewFormPointDecrementFactor = "Score Decrement Factor"
 contestNewFormPointDecrementFactorDesc = "A number between 0 and 1, inclusive. If the number is different from 0, the score awarded for a correct submission will decay as time passes, according to the following formula: (1 - points_decay_factor) + points_decay_factor * TT^2 / (10 * PT^2 + TT^2), where PT is the penalty of the submission in minutes and TT the total length of the contest in minutes."
+contestNewFormRecommended = "Recommended Contest"
+contestNewFormRecommendedTextAdmin = "Enable this option if you want your contest to appear in the Recommended Contests list."
+contestNewFormRecommendedTextNonAdmin = "To show your contest in the Recommended Contests list, contact soporte@omegaup.com."
 contestNewFormScheduleContest = "Schedule contest"
 contestNewFormScheduleVirtualContest = "Schedule virtual contest"
 contestNewFormScoreMode = "Score mode"
diff --git a/frontend/templates/es.lang b/frontend/templates/es.lang
index f35d4e30684..aca1fabd8a7 100644
--- a/frontend/templates/es.lang
+++ b/frontend/templates/es.lang
@@ -398,6 +398,9 @@ contestNewFormPenaltyType = "Tipo de penalización"
 contestNewFormPenaltyTypeDesc = "Indica cómo se calcula la penalización por envío: tiempo desde que inicia el concurso, tiempo desde que se abre el problema, o tiempo de ejecución del programa."
 contestNewFormPointDecrementFactor = "Factor de decremento de puntos"
 contestNewFormPointDecrementFactorDesc = "Un número entre 0 y 1 inclusive. Si este número es distinto de cero, el puntaje que se obtiene al resolver correctamente un problema decae conforme pasa el tiempo. El valor del puntaje estará dado por (1 - points_decay_factor) + points_decay_factor * TT^2 / (10 * PT^2 + TT^2), donde PT es el penalty en minutos del envío y TT el tiempo total del concurso, en minutos."
+contestNewFormRecommended = "Concurso recomendado"
+contestNewFormRecommendedTextAdmin = "Habilita esta opción si deseas que tu concurso aparezca en la lista de Concursos Recomendados."
+contestNewFormRecommendedTextNonAdmin = "Para mostrar tu concurso en la lista de Concursos Recomendados, contacta a soporte@omegaup.com."
 contestNewFormScheduleContest = "Agendar concurso"
 contestNewFormScheduleVirtualContest = "Agendar concurso virtual"
 contestNewFormScoreMode = "Modo de puntaje"
diff --git a/frontend/templates/pseudo.lang b/frontend/templates/pseudo.lang
index f1b414ab4f6..190d7eb47e2 100644
--- a/frontend/templates/pseudo.lang
+++ b/frontend/templates/pseudo.lang
@@ -398,6 +398,9 @@ contestNewFormPenaltyType = "(P3na17y 7yp3)"
 contestNewFormPenaltyTypeDesc = "(Indica735 h0w 7h3 5ubmi55i0n p3na17y i5 ca1cu1a73d: minu735 5inc3 7h3 b3ginning 0f 7h3 c0n7357, minu735 5inc3 7h3 pr0b13m wa5 0p3n3d, 0r pr0gram 3x3cu7i0n 7im3 (mi11i53c0nd5).)"
 contestNewFormPointDecrementFactor = "(Sc0r3 D3cr3m3n7 Fac70r)"
 contestNewFormPointDecrementFactorDesc = "(A numb3r b37w33n 0 and 1, inc1u5iv3. If 7h3 numb3r i5 diff3r3n7 fr0m 0, 7h3 5c0r3 award3d f0r a c0rr3c7 5ubmi55i0n wi11 d3cay a5 7im3 pa5535, acc0rding 70 7h3 f0110wing f0rmu1a: (1 - p0in75_d3cay_fac70r) + p0in75_d3cay_fac70r * TT^2 / (10 * PT^2 + TT^2), wh3r3 PT i5 7h3 p3na17y 0f 7h3 5ubmi55i0n in minu735 and TT 7h3 707a1 13ng7h 0f 7h3 c0n7357 in minu735.)"
+contestNewFormRecommended = "(R3c0mm3nd3d C0n7357)"
+contestNewFormRecommendedTextAdmin = "(Enab13 7hi5 0p7i0n if y0u wan7 y0ur c0n7357 70 app3ar in 7h3 R3c0mm3nd3d C0n73575 1i57.)"
+contestNewFormRecommendedTextNonAdmin = "(T0 5h0w y0ur c0n7357 in 7h3 R3c0mm3nd3d C0n73575 1i57, c0n7ac7 50p0r73@0m3gaup.c0m.)"
 contestNewFormScheduleContest = "(Sch3du13 c0n7357)"
 contestNewFormScheduleVirtualContest = "(Sch3du13 vir7ua1 c0n7357)"
 contestNewFormScoreMode = "(Sc0r3 m0d3)"
diff --git a/frontend/templates/pt.lang b/frontend/templates/pt.lang
index a554ffe4703..74f7f727d0f 100644
--- a/frontend/templates/pt.lang
+++ b/frontend/templates/pt.lang
@@ -398,6 +398,9 @@ contestNewFormPenaltyType = "Tipo de penalização"
 contestNewFormPenaltyTypeDesc = "Ele indica como penalidade frete é calculado: tempo desde que a concurso começou, o tempo já que o problema é aberto, ou tempo de execução do programa."
 contestNewFormPointDecrementFactor = "Fator de decrescimiento de pontuação"
 contestNewFormPointDecrementFactorDesc = "Un número entre 0 e 1 inclusive. Si este número é deferente de zero, a pontuação que se obtem ao resolver corretamente um problema descresce conforme passa o tiempo. O valor da pontuação estará dado por (1 - points_decay_factor) + points_decay_factor * TT ^ 2 / (10 * PT ^ 2 + TT ^ 2), onde PT é a penalidade em minutos do envío e TT o tempo total do concurso, en minutos."
+contestNewFormRecommended = "Concurso recomendado"
+contestNewFormRecommendedTextAdmin = "Ative esta opção se você deseja que seu concurso apareça na lista de Concursos Recomendados."
+contestNewFormRecommendedTextNonAdmin = "Para mostrar seu concurso na lista de Concursos Recomendados, entre em contato com soporte@omegaup.com."
 contestNewFormScheduleContest = "Agendar concurso"
 contestNewFormScheduleVirtualContest = "Agendar concurso virtual"
 contestNewFormScoreMode = "Modo de pontuação"
diff --git a/frontend/tests/Factories/Contest.php b/frontend/tests/Factories/Contest.php
index d577ade1f1d..3208ebdf8c6 100644
--- a/frontend/tests/Factories/Contest.php
+++ b/frontend/tests/Factories/Contest.php
@@ -240,7 +240,6 @@ public static function getRequest(?ContestParams $params = null): array {
             'scoreboard' => $params->scoreboardPct,
             'penalty_type' => $params->penaltyType,
             'languages' => join(',', $params->languages),
-            'recommended' => 0, // This is just a default value, it is not honored by apiCreate.
             'needs_basic_information' => $params->basicInformation,
             'requests_user_information' => $params->requestsUserInformation,
             'penalty_calc_policy' => $params->penaltyCalcPolicy,
diff --git a/frontend/tests/controllers/ContestRecommendedTest.php b/frontend/tests/controllers/ContestRecommendedTest.php
new file mode 100644
index 00000000000..783490a604c
--- /dev/null
+++ b/frontend/tests/controllers/ContestRecommendedTest.php
@@ -0,0 +1,196 @@
+<?php
+/**
+ * ContestRecommendedTest
+ */
+
+class ContestRecommendedTest extends \OmegaUp\Test\ControllerTestCase {
+    /**
+     * Tests that the recommended flag can be set when creating a contest
+     * by a support team member.
+     */
+    public function testCreateContestWithRecommendedFlag() {
+        // Create a support team member
+        ['identity' => $identity] = \OmegaUp\Test\Factories\User::createSupportUser();
+
+        // Create a valid contest Request object
+        $contestData = \OmegaUp\Test\Factories\Contest::getRequest(new \OmegaUp\Test\Factories\ContestParams(
+            ['admissionMode' => 'private']
+        ));
+        $r = $contestData['request'];
+        $r['recommended'] = true;
+
+        // Log in the user and set the auth token in the new request
+        $login = self::login($identity);
+        $r['auth_token'] = $login->auth_token;
+
+        // Call the API
+        $response = \OmegaUp\Controllers\Contest::apiCreate($r);
+        $this->assertSame('ok', $response['status']);
+
+        // Verify the contest was created with recommended flag
+        $contest = \OmegaUp\DAO\Contests::getByAlias($r['alias']);
+        $this->assertTrue($contest->recommended);
+    }
+
+    /**
+     * Tests that non-support team members cannot set the recommended flag
+     * when creating a contest.
+     */
+    public function testCreateContestWithRecommendedFlagAsNonSupportTeamMember() {
+        // Create a regular user (not a support team member)
+        ['identity' => $identity] = \OmegaUp\Test\Factories\User::createUser();
+
+        // Create a valid contest Request object
+        $contestData = \OmegaUp\Test\Factories\Contest::getRequest(new \OmegaUp\Test\Factories\ContestParams(
+            ['admissionMode' => 'private']
+        ));
+        $r = $contestData['request'];
+        $r['recommended'] = true;
+
+        // Log in the user and set the auth token in the new request
+        $login = self::login($identity);
+        $r['auth_token'] = $login->auth_token;
+
+        try {
+            \OmegaUp\Controllers\Contest::apiCreate($r);
+            $this->fail('Should have failed');
+        } catch (\OmegaUp\Exceptions\ForbiddenAccessException $e) {
+            $this->assertSame('userNotAllowed', $e->getMessage());
+        }
+    }
+
+    /**
+     * Tests that the recommended flag can be updated when editing a contest
+     * by a support team member.
+     */
+    public function testUpdateContestRecommendedFlag() {
+        // Create a support team member
+        ['identity' => $identity] = \OmegaUp\Test\Factories\User::createSupportUser();
+
+        // Create a contest first
+        $contestData = \OmegaUp\Test\Factories\Contest::createContest(
+            new \OmegaUp\Test\Factories\ContestParams([
+                'admissionMode' => 'private',
+                'recommended' => false,
+                'contestDirector' => $identity,
+            ])
+        );
+
+        // Update the contest with recommended flag
+        $login = self::login($identity);
+        $response = \OmegaUp\Controllers\Contest::apiUpdate(new \OmegaUp\Request([
+            'auth_token' => $login->auth_token,
+            'contest_alias' => $contestData['request']['alias'],
+            'title' => $contestData['request']['title'],
+            'description' => $contestData['request']['description'],
+            'start_time' => $contestData['request']['start_time'],
+            'finish_time' => $contestData['request']['finish_time'],
+            'recommended' => true,
+        ]));
+        $this->assertSame('ok', $response['status']);
+
+        // Verify the contest was updated with recommended flag
+        $contest = \OmegaUp\DAO\Contests::getByAlias(
+            $contestData['request']['alias']
+        );
+        $this->assertTrue($contest->recommended);
+    }
+
+    /**
+     * Tests that non-support team members cannot update the recommended flag
+     * when editing a contest.
+     */
+    public function testUpdateContestRecommendedFlagAsNonSupportTeamMember() {
+        // Create a regular user (not a support team member)
+        ['identity' => $identity] = \OmegaUp\Test\Factories\User::createUser();
+
+        // Create a contest first with a support team member as director
+        ['identity' => $supportIdentity] = \OmegaUp\Test\Factories\User::createSupportUser();
+        $contestData = \OmegaUp\Test\Factories\Contest::createContest(
+            new \OmegaUp\Test\Factories\ContestParams([
+                'admissionMode' => 'private',
+                'recommended' => false,
+                'contestDirector' => $supportIdentity,
+            ])
+        );
+
+        // Try to update the contest with recommended flag
+        $login = self::login($identity);
+        try {
+            \OmegaUp\Controllers\Contest::apiUpdate(new \OmegaUp\Request([
+                'auth_token' => $login->auth_token,
+                'contest_alias' => $contestData['request']['alias'],
+                'title' => $contestData['request']['title'],
+                'description' => $contestData['request']['description'],
+                'start_time' => $contestData['request']['start_time'],
+                'finish_time' => $contestData['request']['finish_time'],
+                'recommended' => true,
+            ]));
+            $this->fail('Should have failed');
+        } catch (\OmegaUp\Exceptions\ForbiddenAccessException $e) {
+            $this->assertSame('userNotAllowed', $e->getMessage());
+        }
+
+        // Verify the contest was not updated
+        $contest = \OmegaUp\DAO\Contests::getByAlias(
+            $contestData['request']['alias']
+        );
+        $this->assertFalse($contest->recommended);
+    }
+
+    /**
+     * Tests that the recommended flag can be toggled multiple times
+     * by a support team member.
+     */
+    public function testToggleContestRecommendedFlag() {
+        // Create a support team member
+        ['identity' => $identity] = \OmegaUp\Test\Factories\User::createSupportUser();
+
+        // Create a contest first
+        $contestData = \OmegaUp\Test\Factories\Contest::createContest(
+            new \OmegaUp\Test\Factories\ContestParams([
+                'admissionMode' => 'private',
+                'recommended' => false,
+                'contestDirector' => $identity,
+            ])
+        );
+
+        $login = self::login($identity);
+
+        // Toggle recommended flag to true
+        $response = \OmegaUp\Controllers\Contest::apiUpdate(new \OmegaUp\Request([
+            'auth_token' => $login->auth_token,
+            'contest_alias' => $contestData['request']['alias'],
+            'title' => $contestData['request']['title'],
+            'description' => $contestData['request']['description'],
+            'start_time' => $contestData['request']['start_time'],
+            'finish_time' => $contestData['request']['finish_time'],
+            'recommended' => true,
+        ]));
+        $this->assertSame('ok', $response['status']);
+
+        // Verify the contest was updated with recommended flag
+        $contest = \OmegaUp\DAO\Contests::getByAlias(
+            $contestData['request']['alias']
+        );
+        $this->assertTrue($contest->recommended);
+
+        // Toggle recommended flag back to false
+        $response = \OmegaUp\Controllers\Contest::apiUpdate(new \OmegaUp\Request([
+            'auth_token' => $login->auth_token,
+            'contest_alias' => $contestData['request']['alias'],
+            'title' => $contestData['request']['title'],
+            'description' => $contestData['request']['description'],
+            'start_time' => $contestData['request']['start_time'],
+            'finish_time' => $contestData['request']['finish_time'],
+            'recommended' => false,
+        ]));
+        $this->assertSame('ok', $response['status']);
+
+        // Verify the contest was updated with recommended flag
+        $contest = \OmegaUp\DAO\Contests::getByAlias(
+            $contestData['request']['alias']
+        );
+        $this->assertFalse($contest->recommended);
+    }
+}
diff --git a/frontend/www/js/omegaup/api_types.ts b/frontend/www/js/omegaup/api_types.ts
index 122b2c55458..65a483d8d28 100755
--- a/frontend/www/js/omegaup/api_types.ts
+++ b/frontend/www/js/omegaup/api_types.ts
@@ -3014,6 +3014,7 @@ export namespace types {
     alias: string;
     archived: boolean;
     available_languages: { [key: string]: string };
+    canSetRecommended: boolean;
     contest_for_teams: boolean;
     default_show_all_contestants_in_scoreboard: boolean;
     description: string;
@@ -3032,6 +3033,7 @@ export namespace types {
     points_decay_factor: number;
     problems?: types.ProblemsetProblem[];
     problemset_id: number;
+    recommended?: boolean;
     requests_user_information: string;
     rerun_id?: number;
     score_mode: string;
@@ -3076,6 +3078,7 @@ export namespace types {
     points_decay_factor: number;
     problems: types.ProblemsetProblem[];
     problemset_id: number;
+    recommended: boolean;
     requests_user_information: string;
     rerun_id?: number;
     score_mode: string;
@@ -3192,6 +3195,7 @@ export namespace types {
   }
 
   export interface ContestNewPayload {
+    canSetRecommended?: boolean;
     hasVisitedSection?: boolean;
     languages: { [key: string]: string };
   }
diff --git a/frontend/www/js/omegaup/components/contest/Edit.test.ts b/frontend/www/js/omegaup/components/contest/Edit.test.ts
index 079c0080076..2e981be4e31 100644
--- a/frontend/www/js/omegaup/components/contest/Edit.test.ts
+++ b/frontend/www/js/omegaup/components/contest/Edit.test.ts
@@ -12,6 +12,8 @@ describe('Edit.vue', () => {
     alias: 'test',
     archived: false,
     available_languages: { py2: 'py2' },
+    canSetRecommended: false,
+    recommended: false,
     contest_for_teams: false,
     default_show_all_contestants_in_scoreboard: false,
     description: 'contest test',
diff --git a/frontend/www/js/omegaup/components/contest/Edit.vue b/frontend/www/js/omegaup/components/contest/Edit.vue
index c04b372e98a..25c37fc95b6 100644
--- a/frontend/www/js/omegaup/components/contest/Edit.vue
+++ b/frontend/www/js/omegaup/components/contest/Edit.vue
@@ -154,6 +154,8 @@
           :update="true"
           :search-result-teams-groups="searchResultTeamsGroups"
           :problems="problems"
+          :can-set-recommended="details.canSetRecommended"
+          :initial-recommended="details.recommended"
           @update-search-result-teams-groups="
             (query) => $emit('update-search-result-teams-groups', query)
           "
diff --git a/frontend/www/js/omegaup/components/contest/NewForm.vue b/frontend/www/js/omegaup/components/contest/NewForm.vue
index b9e594f96a4..7c82d3c545c 100644
--- a/frontend/www/js/omegaup/components/contest/NewForm.vue
+++ b/frontend/www/js/omegaup/components/contest/NewForm.vue
@@ -234,9 +234,7 @@
               :existing-options="searchResultTeamsGroups"
               :options="searchResultTeamsGroups"
               :value.sync="currentTeamsGroupAlias"
-              @update-existing-options="
-                (query) => $emit('update-search-result-teams-groups', query)
-              "
+              @update-existing-options="updateTeamsGroups"
             >
             </omegaup-common-typeahead>
             <input
@@ -357,6 +355,27 @@
             </p>
           </div>
         </div>
+        <div class="row">
+          <div class="form-group col-md-6">
+            <label>{{ T.contestNewFormRecommended }}</label>
+            <div v-if="canSetRecommended" class="checkbox form-check">
+              <input
+                v-model="recommended"
+                data-recommended
+                class="form-check-input"
+                type="checkbox"
+              />
+              <label class="form-check-label"> {{ T.wordsEnable }}</label>
+            </div>
+            <p class="help-block">
+              {{
+                canSetRecommended
+                  ? T.contestNewFormRecommendedTextAdmin
+                  : T.contestNewFormRecommendedTextNonAdmin
+              }}
+            </p>
+          </div>
+        </div>
         <div class="form-group">
           <button class="btn btn-primary introjs-schedule" type="submit">
             {{
@@ -425,6 +444,8 @@ export default class NewForm extends Vue {
   @Prop({ default: false }) contestForTeams!: boolean;
   @Prop({ default: null }) problems!: types.ProblemsetProblemWithVersions[];
   @Prop({ default: true }) hasVisitedSection!: boolean;
+  @Prop({ default: false }) canSetRecommended!: boolean;
+  @Prop({ default: false }) initialRecommended!: boolean;
 
   T = T;
   ScoreMode = ScoreMode;
@@ -451,6 +472,7 @@ export default class NewForm extends Vue {
   currentContestForTeams = this.contestForTeams;
   currentTeamsGroupAlias = this.teamsGroupAlias;
   titlePlaceHolder = '';
+  recommended = this.initialRecommended;
 
   mounted() {
     const title = T.createContestInteractiveGuideTitle;
@@ -584,6 +606,7 @@ export default class NewForm extends Vue {
       alias: this.alias,
       archived: false,
       available_languages: {},
+      canSetRecommended: false,
       director: '',
       opened: false,
       penalty_calc_policy: 'sum',
@@ -609,6 +632,12 @@ export default class NewForm extends Vue {
       requests_user_information: this.requestsUserInformation,
       contest_for_teams: this.currentContestForTeams,
     };
+
+    // Only include recommended field if user has permission
+    if (this.canSetRecommended) {
+      contest.recommended = this.recommended;
+    }
+
     if (this.windowLengthEnabled && this.windowLength) {
       contest.window_length = this.windowLength;
     }
@@ -658,6 +687,10 @@ export default class NewForm extends Vue {
   onSelect(language: string) {
     this.languages.push(language);
   }
+
+  updateTeamsGroups(query: string) {
+    this.$emit('update-search-result-teams-groups', query);
+  }
 }
 </script>
 
diff --git a/frontend/www/js/omegaup/contest/new.ts b/frontend/www/js/omegaup/contest/new.ts
index 87515933f00..0e3740287bd 100644
--- a/frontend/www/js/omegaup/contest/new.ts
+++ b/frontend/www/js/omegaup/contest/new.ts
@@ -17,6 +17,7 @@ OmegaUp.on('ready', () => {
     data: () => ({
       invalidParameterName: null as null | string,
       searchResultTeamsGroups: [] as types.ListItem[],
+      canSetRecommended: payload.canSetRecommended,
     }),
     render: function (createElement) {
       return createElement('omegaup-contest-new', {
@@ -29,6 +30,7 @@ OmegaUp.on('ready', () => {
           invalidParameterName: this.invalidParameterName,
           searchResultTeamsGroups: this.searchResultTeamsGroups,
           hasVisitedSection: payload.hasVisitedSection,
+          canSetRecommended: this.canSetRecommended,
         },
         on: {
           'create-contest': ({
diff --git a/frontend/www/js/omegaup/lang.en.json b/frontend/www/js/omegaup/lang.en.json
index b491277dc84..32e536c7751 100644
--- a/frontend/www/js/omegaup/lang.en.json
+++ b/frontend/www/js/omegaup/lang.en.json
@@ -399,6 +399,9 @@
 	"contestNewFormPenaltyTypeDesc": "Indicates how the submission penalty is calculated: minutes since the beginning of the contest, minutes since the problem was opened, or program execution time (milliseconds).",
 	"contestNewFormPointDecrementFactor": "Score Decrement Factor",
 	"contestNewFormPointDecrementFactorDesc": "A number between 0 and 1, inclusive. If the number is different from 0, the score awarded for a correct submission will decay as time passes, according to the following formula: (1 - points_decay_factor) + points_decay_factor * TT^2 / (10 * PT^2 + TT^2), where PT is the penalty of the submission in minutes and TT the total length of the contest in minutes.",
+	"contestNewFormRecommended": "Recommended Contest",
+	"contestNewFormRecommendedTextAdmin": "Enable this option if you want your contest to appear in the Recommended Contests list.",
+	"contestNewFormRecommendedTextNonAdmin": "To show your contest in the Recommended Contests list, contact soporte@omegaup.com.",
 	"contestNewFormScheduleContest": "Schedule contest",
 	"contestNewFormScheduleVirtualContest": "Schedule virtual contest",
 	"contestNewFormScoreMode": "Score mode",
diff --git a/frontend/www/js/omegaup/lang.en.ts b/frontend/www/js/omegaup/lang.en.ts
index e3c338d1f33..7bd129ffee9 100644
--- a/frontend/www/js/omegaup/lang.en.ts
+++ b/frontend/www/js/omegaup/lang.en.ts
@@ -400,6 +400,9 @@ const translations: { [key: string]: string; } = {
   contestNewFormPenaltyTypeDesc: "Indicates how the submission penalty is calculated: minutes since the beginning of the contest, minutes since the problem was opened, or program execution time (milliseconds).",
   contestNewFormPointDecrementFactor: "Score Decrement Factor",
   contestNewFormPointDecrementFactorDesc: "A number between 0 and 1, inclusive. If the number is different from 0, the score awarded for a correct submission will decay as time passes, according to the following formula: (1 - points_decay_factor) + points_decay_factor * TT^2 / (10 * PT^2 + TT^2), where PT is the penalty of the submission in minutes and TT the total length of the contest in minutes.",
+  contestNewFormRecommended: "Recommended Contest",
+  contestNewFormRecommendedTextAdmin: "Enable this option if you want your contest to appear in the Recommended Contests list.",
+  contestNewFormRecommendedTextNonAdmin: "To show your contest in the Recommended Contests list, contact soporte@omegaup.com.",
   contestNewFormScheduleContest: "Schedule contest",
   contestNewFormScheduleVirtualContest: "Schedule virtual contest",
   contestNewFormScoreMode: "Score mode",
diff --git a/frontend/www/js/omegaup/lang.es.json b/frontend/www/js/omegaup/lang.es.json
index 53bbe78557e..9edef346425 100644
--- a/frontend/www/js/omegaup/lang.es.json
+++ b/frontend/www/js/omegaup/lang.es.json
@@ -399,6 +399,9 @@
 	"contestNewFormPenaltyTypeDesc": "Indica c\u00f3mo se calcula la penalizaci\u00f3n por env\u00edo: tiempo desde que inicia el concurso, tiempo desde que se abre el problema, o tiempo de ejecuci\u00f3n del programa.",
 	"contestNewFormPointDecrementFactor": "Factor de decremento de puntos",
 	"contestNewFormPointDecrementFactorDesc": "Un n\u00famero entre 0 y 1 inclusive. Si este n\u00famero es distinto de cero, el puntaje que se obtiene al resolver correctamente un problema decae conforme pasa el tiempo. El valor del puntaje estar\u00e1 dado por (1 - points_decay_factor) + points_decay_factor * TT^2 / (10 * PT^2 + TT^2), donde PT es el penalty en minutos del env\u00edo y TT el tiempo total del concurso, en minutos.",
+	"contestNewFormRecommended": "Concurso recomendado",
+	"contestNewFormRecommendedTextAdmin": "Habilita esta opci\u00f3n si deseas que tu concurso aparezca en la lista de Concursos Recomendados.",
+	"contestNewFormRecommendedTextNonAdmin": "Para mostrar tu concurso en la lista de Concursos Recomendados, contacta a soporte@omegaup.com.",
 	"contestNewFormScheduleContest": "Agendar concurso",
 	"contestNewFormScheduleVirtualContest": "Agendar concurso virtual",
 	"contestNewFormScoreMode": "Modo de puntaje",
diff --git a/frontend/www/js/omegaup/lang.es.ts b/frontend/www/js/omegaup/lang.es.ts
index a101016d2fb..96cddab5f1a 100644
--- a/frontend/www/js/omegaup/lang.es.ts
+++ b/frontend/www/js/omegaup/lang.es.ts
@@ -400,6 +400,9 @@ const translations: { [key: string]: string; } = {
   contestNewFormPenaltyTypeDesc: "Indica c\u00f3mo se calcula la penalizaci\u00f3n por env\u00edo: tiempo desde que inicia el concurso, tiempo desde que se abre el problema, o tiempo de ejecuci\u00f3n del programa.",
   contestNewFormPointDecrementFactor: "Factor de decremento de puntos",
   contestNewFormPointDecrementFactorDesc: "Un n\u00famero entre 0 y 1 inclusive. Si este n\u00famero es distinto de cero, el puntaje que se obtiene al resolver correctamente un problema decae conforme pasa el tiempo. El valor del puntaje estar\u00e1 dado por (1 - points_decay_factor) + points_decay_factor * TT^2 / (10 * PT^2 + TT^2), donde PT es el penalty en minutos del env\u00edo y TT el tiempo total del concurso, en minutos.",
+  contestNewFormRecommended: "Concurso recomendado",
+  contestNewFormRecommendedTextAdmin: "Habilita esta opci\u00f3n si deseas que tu concurso aparezca en la lista de Concursos Recomendados.",
+  contestNewFormRecommendedTextNonAdmin: "Para mostrar tu concurso en la lista de Concursos Recomendados, contacta a soporte@omegaup.com.",
   contestNewFormScheduleContest: "Agendar concurso",
   contestNewFormScheduleVirtualContest: "Agendar concurso virtual",
   contestNewFormScoreMode: "Modo de puntaje",
diff --git a/frontend/www/js/omegaup/lang.pseudo.json b/frontend/www/js/omegaup/lang.pseudo.json
index 22c942b73ac..2f1e4837b77 100644
--- a/frontend/www/js/omegaup/lang.pseudo.json
+++ b/frontend/www/js/omegaup/lang.pseudo.json
@@ -399,6 +399,9 @@
 	"contestNewFormPenaltyTypeDesc": "(Indica735 h0w 7h3 5ubmi55i0n p3na17y i5 ca1cu1a73d: minu735 5inc3 7h3 b3ginning 0f 7h3 c0n7357, minu735 5inc3 7h3 pr0b13m wa5 0p3n3d, 0r pr0gram 3x3cu7i0n 7im3 (mi11i53c0nd5).)",
 	"contestNewFormPointDecrementFactor": "(Sc0r3 D3cr3m3n7 Fac70r)",
 	"contestNewFormPointDecrementFactorDesc": "(A numb3r b37w33n 0 and 1, inc1u5iv3. If 7h3 numb3r i5 diff3r3n7 fr0m 0, 7h3 5c0r3 award3d f0r a c0rr3c7 5ubmi55i0n wi11 d3cay a5 7im3 pa5535, acc0rding 70 7h3 f0110wing f0rmu1a: (1 - p0in75_d3cay_fac70r) + p0in75_d3cay_fac70r * TT^2 / (10 * PT^2 + TT^2), wh3r3 PT i5 7h3 p3na17y 0f 7h3 5ubmi55i0n in minu735 and TT 7h3 707a1 13ng7h 0f 7h3 c0n7357 in minu735.)",
+	"contestNewFormRecommended": "(R3c0mm3nd3d C0n7357)",
+	"contestNewFormRecommendedTextAdmin": "(Enab13 7hi5 0p7i0n if y0u wan7 y0ur c0n7357 70 app3ar in 7h3 R3c0mm3nd3d C0n73575 1i57.)",
+	"contestNewFormRecommendedTextNonAdmin": "(T0 5h0w y0ur c0n7357 in 7h3 R3c0mm3nd3d C0n73575 1i57, c0n7ac7 50p0r73@0m3gaup.c0m.)",
 	"contestNewFormScheduleContest": "(Sch3du13 c0n7357)",
 	"contestNewFormScheduleVirtualContest": "(Sch3du13 vir7ua1 c0n7357)",
 	"contestNewFormScoreMode": "(Sc0r3 m0d3)",
diff --git a/frontend/www/js/omegaup/lang.pseudo.ts b/frontend/www/js/omegaup/lang.pseudo.ts
index 914bd8356af..6fc5d81ce0c 100644
--- a/frontend/www/js/omegaup/lang.pseudo.ts
+++ b/frontend/www/js/omegaup/lang.pseudo.ts
@@ -400,6 +400,9 @@ const translations: { [key: string]: string; } = {
   contestNewFormPenaltyTypeDesc: "(Indica735 h0w 7h3 5ubmi55i0n p3na17y i5 ca1cu1a73d: minu735 5inc3 7h3 b3ginning 0f 7h3 c0n7357, minu735 5inc3 7h3 pr0b13m wa5 0p3n3d, 0r pr0gram 3x3cu7i0n 7im3 (mi11i53c0nd5).)",
   contestNewFormPointDecrementFactor: "(Sc0r3 D3cr3m3n7 Fac70r)",
   contestNewFormPointDecrementFactorDesc: "(A numb3r b37w33n 0 and 1, inc1u5iv3. If 7h3 numb3r i5 diff3r3n7 fr0m 0, 7h3 5c0r3 award3d f0r a c0rr3c7 5ubmi55i0n wi11 d3cay a5 7im3 pa5535, acc0rding 70 7h3 f0110wing f0rmu1a: (1 - p0in75_d3cay_fac70r) + p0in75_d3cay_fac70r * TT^2 / (10 * PT^2 + TT^2), wh3r3 PT i5 7h3 p3na17y 0f 7h3 5ubmi55i0n in minu735 and TT 7h3 707a1 13ng7h 0f 7h3 c0n7357 in minu735.)",
+  contestNewFormRecommended: "(R3c0mm3nd3d C0n7357)",
+  contestNewFormRecommendedTextAdmin: "(Enab13 7hi5 0p7i0n if y0u wan7 y0ur c0n7357 70 app3ar in 7h3 R3c0mm3nd3d C0n73575 1i57.)",
+  contestNewFormRecommendedTextNonAdmin: "(T0 5h0w y0ur c0n7357 in 7h3 R3c0mm3nd3d C0n73575 1i57, c0n7ac7 50p0r73@0m3gaup.c0m.)",
   contestNewFormScheduleContest: "(Sch3du13 c0n7357)",
   contestNewFormScheduleVirtualContest: "(Sch3du13 vir7ua1 c0n7357)",
   contestNewFormScoreMode: "(Sc0r3 m0d3)",
diff --git a/frontend/www/js/omegaup/lang.pt.json b/frontend/www/js/omegaup/lang.pt.json
index aebedc168f4..6a2dd405ae0 100644
--- a/frontend/www/js/omegaup/lang.pt.json
+++ b/frontend/www/js/omegaup/lang.pt.json
@@ -399,6 +399,9 @@
 	"contestNewFormPenaltyTypeDesc": "Ele indica como penalidade frete \u00e9 calculado: tempo desde que a concurso come\u00e7ou, o tempo j\u00e1 que o problema \u00e9 aberto, ou tempo de execu\u00e7\u00e3o do programa.",
 	"contestNewFormPointDecrementFactor": "Fator de decrescimiento de pontua\u00e7\u00e3o",
 	"contestNewFormPointDecrementFactorDesc": "Un n\u00famero entre 0 e 1 inclusive. Si este n\u00famero \u00e9 deferente de zero, a pontua\u00e7\u00e3o que se obtem ao resolver corretamente um problema descresce conforme passa o tiempo. O valor da pontua\u00e7\u00e3o estar\u00e1 dado por (1 - points_decay_factor) + points_decay_factor * TT ^ 2 / (10 * PT ^ 2 + TT ^ 2), onde PT \u00e9 a penalidade em minutos do env\u00edo e TT o tempo total do concurso, en minutos.",
+	"contestNewFormRecommended": "Concurso recomendado",
+	"contestNewFormRecommendedTextAdmin": "Ative esta op\u00e7\u00e3o se voc\u00ea deseja que seu concurso apare\u00e7a na lista de Concursos Recomendados.",
+	"contestNewFormRecommendedTextNonAdmin": "Para mostrar seu concurso na lista de Concursos Recomendados, entre em contato com soporte@omegaup.com.",
 	"contestNewFormScheduleContest": "Agendar concurso",
 	"contestNewFormScheduleVirtualContest": "Agendar concurso virtual",
 	"contestNewFormScoreMode": "Modo de pontua\u00e7\u00e3o",
diff --git a/frontend/www/js/omegaup/lang.pt.ts b/frontend/www/js/omegaup/lang.pt.ts
index e7672ef19c1..f3acc29ced4 100644
--- a/frontend/www/js/omegaup/lang.pt.ts
+++ b/frontend/www/js/omegaup/lang.pt.ts
@@ -400,6 +400,9 @@ const translations: { [key: string]: string; } = {
   contestNewFormPenaltyTypeDesc: "Ele indica como penalidade frete \u00e9 calculado: tempo desde que a concurso come\u00e7ou, o tempo j\u00e1 que o problema \u00e9 aberto, ou tempo de execu\u00e7\u00e3o do programa.",
   contestNewFormPointDecrementFactor: "Fator de decrescimiento de pontua\u00e7\u00e3o",
   contestNewFormPointDecrementFactorDesc: "Un n\u00famero entre 0 e 1 inclusive. Si este n\u00famero \u00e9 deferente de zero, a pontua\u00e7\u00e3o que se obtem ao resolver corretamente um problema descresce conforme passa o tiempo. O valor da pontua\u00e7\u00e3o estar\u00e1 dado por (1 - points_decay_factor) + points_decay_factor * TT ^ 2 / (10 * PT ^ 2 + TT ^ 2), onde PT \u00e9 a penalidade em minutos do env\u00edo e TT o tempo total do concurso, en minutos.",
+  contestNewFormRecommended: "Concurso recomendado",
+  contestNewFormRecommendedTextAdmin: "Ative esta op\u00e7\u00e3o se voc\u00ea deseja que seu concurso apare\u00e7a na lista de Concursos Recomendados.",
+  contestNewFormRecommendedTextNonAdmin: "Para mostrar seu concurso na lista de Concursos Recomendados, entre em contato com soporte@omegaup.com.",
   contestNewFormScheduleContest: "Agendar concurso",
   contestNewFormScheduleVirtualContest: "Agendar concurso virtual",
   contestNewFormScoreMode: "Modo de pontua\u00e7\u00e3o",


Reviews:
user: login: pabo99
body: Thank you very much for resolving the issue on both sides (client and server). Since no one claimed the server-side part, it's completely valid that you resolved it!
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T13:40:29Z
user: login: pabo99
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-06T13:53:16Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T14:16:35Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T14:18:35Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T14:20:57Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T14:21:10Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T14:21:23Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T14:21:35Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T14:21:54Z
user: login: pabo99
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-06T17:31:34Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T19:39:35Z
user: login: pabo99
body: Please apply the suggested comments, and then we can check if the GitHub workflows pass correctly
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T07:40:34Z
user: login: pabo99
body: I left some suggestions that might help fix the GitHub workflow errors
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T19:42:20Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T20:30:38Z
user: login: pabo99
body: Two more changes and it’s good to go!

Also, you might need to update your branch to fix the other errors shown here.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T15:23:59Z
user: login: pabo99
body: Don't forget run the command:
```bash
./stuff/lint.sh
````
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T19:22:38Z
user: login: pabo99
body: Thank you so much!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-20T14:18:08Z
user: login: heduenas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-20T19:44:24Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T10:54:03Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T10:54:39Z
user: login: Avadhutgiri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T13:34:22Z
user: login: heduenas
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-27T03:31:16Z

Review comments:
diff_hunk: @@ -357,6 +357,23 @@
             </p>
           </div>
         </div>
+        <div class="row" v-if="isSystemAdminOrSupport">
+          <div class="form-group col-md-6">
+            <label>{{ T.contestFilterByRecommended }}</label>
+            <div class="checkbox form-check">
+              <input
+                v-model="recommended"
+                data-recommended
+                class="form-check-input"
+                type="checkbox"
+              />
+              <label class="form-check-label"> {{ T.wordsEnable }}</label>
+            </div>
+            <p class="help-block">
+              {{ T.arenaPageRecommendedContestsText }}
+            </p>
path: frontend/www/js/omegaup/components/contest/NewForm.vue
user: login: pabo99
body: Here you could add a condition so the message changes for those who can perform the action and those who cannot.

For those who cannot, it could remain as: "To show your contest in the Recommended Contests list, contact soporte@omegaup.com."

For those who can, it could be something like: "Enable this option if you want your contest to appear in the Recommended Contests list."

Remember to add it in all three languages.
created_at: 2025-04-04T13:29:36Z
author_association: COLLABORATOR
diff_hunk: @@ -1379,6 +1379,8 @@ public static function getContestNewForTypeScript(
                     'hasVisitedSection' => \OmegaUp\UITools::hasVisitedSection(
                         'has-visited-create-contest'
                     ),
+                    'isSystemAdminOrSupport' => \OmegaUp\Authorization::isSystemAdmin($r->identity) || 
+                        \OmegaUp\Authorization::isSupportTeamMember($r->identity),
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: Since the authorization system we use is inherited, could you omit the call to the isSystemAdmin function? This way, you would still grant access to the sysadmin.

Additionally, could you rename it to `canSetRecommended`?

In the end, it should look like this:
```suggestion
                    'isSystemAdminOrSupport' => \OmegaUp\Authorization::isSupportTeamMember($r->identity),
```

You can apply this in the other places where you use it.


created_at: 2025-04-04T13:32:41Z
author_association: COLLABORATOR
diff_hunk: @@ -2671,6 +2676,19 @@ public static function apiCreate(\OmegaUp\Request $r) {
             ['partial','all_or_nothing','max_per_group'],
         );
         $checkPlagiarism = $r->ensureOptionalBool('check_plagiarism') ?? false;
+        
+        // Handle recommended flag - only available for admins and support team
+        $recommended = false;
+        if ($r->ensureOptionalBool('recommended') ?? false) {
+            if (!\OmegaUp\Authorization::isSystemAdmin($r->identity) && 
+                !\OmegaUp\Authorization::isSupportTeamMember($r->identity)) {
+                throw new \OmegaUp\Exceptions\ForbiddenAccessException(
+                    'userNotAllowed'
+                );
+            }
+            $recommended = true;
+        }
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: What do you think if we only set the value of `recommended` when a user has a valid role?

Since the UI shouldn't display this field for regular users, we should only omit the value if someone calls the API from outside the interface. Something like:
```suggestion
        if (\OmegaUp\Authorization::isSupportTeamMember($r->identity)) {
            $recommended = $r->ensureOptionalBool('recommended') ?? false;
        }
```
By the way, you'll need to add `recommended` as a `request-param` at the beginning of the function.
created_at: 2025-04-04T13:34:07Z
author_association: COLLABORATOR
diff_hunk: @@ -4855,6 +4877,17 @@ public static function apiUpdate(\OmegaUp\Request $r): array {
 
         $updateProblemset = true;
         $updateRequests = false;
+        
+        // Handle recommended flag - only available for admins and support team
+        if ($r->ensureOptionalBool('recommended') !== null) {
+            if (!\OmegaUp\Authorization::isSystemAdmin($r->identity) && 
+                !\OmegaUp\Authorization::isSupportTeamMember($r->identity)) {
+                throw new \OmegaUp\Exceptions\ForbiddenAccessException(
+                    'userNotAllowed'
+                );
+            }
+        }
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: Maybe here it does make sense to throw the exception, although the nesting of conditions can be reduced:
```suggestion
        $recommended = $r->ensureOptionalBool('recommended');
        if (!is_null($recommended) && !\OmegaUp\Authorization::isSupportTeamMember($r->identity)) {
            throw new \OmegaUp\Exceptions\ForbiddenAccessException(
                'userNotAllowed'
            );
        }
```
created_at: 2025-04-04T13:35:02Z
author_association: COLLABORATOR
diff_hunk: @@ -357,6 +357,23 @@
             </p>
           </div>
         </div>
+        <div class="row" v-if="isSystemAdminOrSupport">
path: frontend/www/js/omegaup/components/contest/NewForm.vue
user: login: pabo99
body: I'm wondering if this div should be shown to all users, and the only thing we would change is the text to display, along with hiding the checkbox. So, you could remove the condition for now.
created_at: 2025-04-04T13:35:58Z
author_association: COLLABORATOR
diff_hunk: @@ -357,6 +357,23 @@
             </p>
           </div>
         </div>
+        <div class="row" v-if="isSystemAdminOrSupport">
+          <div class="form-group col-md-6">
+            <label>{{ T.contestFilterByRecommended }}</label>
path: frontend/www/js/omegaup/components/contest/NewForm.vue
user: login: pabo99
body: This string indicates the text: `Only Recommended`, which is useful when we are filtering contests.

What do you think about adding a new text to the translation files?

I suggest:
```ts
contestNewFormRecommended = "Recommended"
``` 

You need to add this in the three translation files: `en.lang`, `es.lang`, `pt.lang` in their respective languages.
created_at: 2025-04-04T13:37:32Z
author_association: COLLABORATOR
diff_hunk: @@ -357,6 +357,23 @@
             </p>
           </div>
         </div>
+        <div class="row" v-if="isSystemAdminOrSupport">
+          <div class="form-group col-md-6">
+            <label>{{ T.contestFilterByRecommended }}</label>
+            <div class="checkbox form-check">
+              <input
+                v-model="recommended"
+                data-recommended
+                class="form-check-input"
+                type="checkbox"
+              />
+              <label class="form-check-label"> {{ T.wordsEnable }}</label>
path: frontend/www/js/omegaup/components/contest/NewForm.vue
user: login: pabo99
body: According to the previous comment, this should be displayed to admins and support team members only.
created_at: 2025-04-04T13:38:04Z
author_association: COLLABORATOR
diff_hunk: @@ -2671,6 +2675,20 @@ public static function apiCreate(\OmegaUp\Request $r) {
             ['partial','all_or_nothing','max_per_group'],
         );
         $checkPlagiarism = $r->ensureOptionalBool('check_plagiarism') ?? false;
+        
+        // Handle recommended flag - only available for admins and support team
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: Oh, this change is even more complicated to understand than how you originally proposed it. But no worries, let's keep moving forward.
created_at: 2025-04-06T13:46:16Z
author_association: COLLABORATOR
diff_hunk: @@ -2671,6 +2675,20 @@ public static function apiCreate(\OmegaUp\Request $r) {
             ['partial','all_or_nothing','max_per_group'],
         );
         $checkPlagiarism = $r->ensureOptionalBool('check_plagiarism') ?? false;
+        
+        // Handle recommended flag - only available for admins and support team
+        $recommendedValue = false;
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: If in the following line you validate that the user is a sysadmin or a support team member, I don't see the need to add an intermediate variable. So it can be removed. It will throw an exception anyway for those who don't have permission to perform this action
created_at: 2025-04-06T13:47:52Z
author_association: COLLABORATOR
diff_hunk: @@ -2671,6 +2675,20 @@ public static function apiCreate(\OmegaUp\Request $r) {
             ['partial','all_or_nothing','max_per_group'],
         );
         $checkPlagiarism = $r->ensureOptionalBool('check_plagiarism') ?? false;
+        
+        // Handle recommended flag - only available for admins and support team
+        $recommendedValue = false;
+        $recommended = $r->ensureOptionalBool('recommended');
+        if (!is_null($recommended)) {
+            if (!\OmegaUp\Authorization::isSystemAdmin($r->identity) && 
+                !\OmegaUp\Authorization::isSupportTeamMember($r->identity)) {
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: Ok, to avoid nesting conditions, you could just use:
```suggestion
        if (
            !is_null($recommended) &&
            !\OmegaUp\Authorization::isSupportTeamMember($r->identity)
        ) {
```
?
created_at: 2025-04-06T13:48:25Z
author_association: COLLABORATOR
diff_hunk: @@ -4855,6 +4877,23 @@ public static function apiUpdate(\OmegaUp\Request $r): array {
 
         $updateProblemset = true;
         $updateRequests = false;
+        
+        // Handle recommended flag - only available for admins and support team
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: Same case as previous comment
created_at: 2025-04-06T13:49:09Z
author_association: COLLABORATOR
diff_hunk: @@ -4922,6 +4961,7 @@ function ($value): ?string {
             ],
             'admission_mode',
             'check_plagiarism',
+            //'recommended', // Removed from auto-update as it's handled manually
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: If you’re not going to use the code, it’s better to remove the line instead of commenting it out
created_at: 2025-04-06T13:49:36Z
author_association: COLLABORATOR
diff_hunk: @@ -757,7 +758,7 @@ groupNotFound = "Group not found"
 groupScoreboardDetailsCoder = "Coder"
 groupsCanNotBeAddedInContestForTeams = "Groups cannot be added in contests for teams"
 groupsCreateNew = "New group"
-groupsCsvHelp = "<p>Quickly add multiple identities to this group. Identities are not regular accounts, and don't need to have an email or validate their account.</p>\n<p>Identities should be in a <code>.csv</code> file with the following format:\n<samp><pre>username,name,country_id,state_id,gender,school_name\nusername-1,Developer Diana,MX,AGU,female,Best School\nusername-2,Danny Dev,MX,AGU,male,Best School</pre></samp></p>"
+groupsCsvHelp = "<p>Quickly add multiple identities to this group. Identities are not regular accounts, and don't need to have an email or validate their account.</p>\n<p>Identities should be in a <code>.csv</code> file with the following format:\n<samp><pre>username,name,country_id,state_id,gender,school_name\nteam-1,Team 1,MX,AGU,other,Best School\nteam-2,Teams 2,MX,AGU,decline,Best School\n```"
path: frontend/templates/en.lang
user: login: pabo99
body: What is this change needed for? If there’s no specific reason, could you revert it?
created_at: 2025-04-06T13:50:06Z
author_association: COLLABORATOR
diff_hunk: @@ -357,6 +355,23 @@
             </p>
           </div>
         </div>
+        <div class="row">
+          <div class="form-group col-md-6">
+            <label>{{ T.contestFilterByRecommended }}</label>
path: frontend/www/js/omegaup/components/contest/NewForm.vue
user: login: pabo99
body: Here you should use `contestNewFormRecommended`, right?
created_at: 2025-04-06T13:50:41Z
author_association: COLLABORATOR
diff_hunk: @@ -357,6 +355,23 @@
             </p>
           </div>
         </div>
+        <div class="row">
+          <div class="form-group col-md-6">
+            <label>{{ T.contestFilterByRecommended }}</label>
+            <div v-if="canSetRecommended" class="checkbox form-check">
+              <input
+                v-model="recommended"
+                data-recommended
+                class="form-check-input"
+                type="checkbox"
+              />
+              <label class="form-check-label"> {{ T.wordsEnable }}</label>
+            </div>
+            <p class="help-block">
+              {{ canSetRecommended ? T.arenaPageRecommendedContestsTextAdmin : T.arenaPageRecommendedContestsTextNonAdmin }}
path: frontend/www/js/omegaup/components/contest/NewForm.vue
user: login: pabo99
body: These text strings don't exist in the translation files, so they would need to be created. Also, I think the suffix should indicate the context in which they're used. It could be something like: `contestNewForm...`
created_at: 2025-04-06T13:51:49Z
author_association: COLLABORATOR
diff_hunk: @@ -4855,6 +4877,23 @@ public static function apiUpdate(\OmegaUp\Request $r): array {
 
         $updateProblemset = true;
         $updateRequests = false;
+        
+        // Handle recommended flag - only available for admins and support team
path: frontend/server/src/Controllers/Contest.php
user: login: Avadhutgiri
body: Can You please Elaborate on this ??
Sorry I didn't understand this

created_at: 2025-04-06T14:16:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2671,6 +2675,20 @@ public static function apiCreate(\OmegaUp\Request $r) {
             ['partial','all_or_nothing','max_per_group'],
         );
         $checkPlagiarism = $r->ensureOptionalBool('check_plagiarism') ?? false;
+        
+        // Handle recommended flag - only available for admins and support team
+        $recommendedValue = false;
path: frontend/server/src/Controllers/Contest.php
user: login: Avadhutgiri
body: Yes, You are right. 
I have made the changes.
created_at: 2025-04-06T14:18:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2671,6 +2675,20 @@ public static function apiCreate(\OmegaUp\Request $r) {
             ['partial','all_or_nothing','max_per_group'],
         );
         $checkPlagiarism = $r->ensureOptionalBool('check_plagiarism') ?? false;
+        
+        // Handle recommended flag - only available for admins and support team
+        $recommendedValue = false;
+        $recommended = $r->ensureOptionalBool('recommended');
+        if (!is_null($recommended)) {
+            if (!\OmegaUp\Authorization::isSystemAdmin($r->identity) && 
+                !\OmegaUp\Authorization::isSupportTeamMember($r->identity)) {
path: frontend/server/src/Controllers/Contest.php
user: login: Avadhutgiri
body: Here 
if ( !is_null($recommended) && 
    !\OmegaUp\Authorization::isSystemAdmin($r->identity) && 
    !\OmegaUp\Authorization::isSupportTeamMember($r->identity )
    ) {
    ..
    }
    I have changed to this SO it would not throw exception for SystemAdmin too .
    is it okay??

created_at: 2025-04-06T14:20:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4922,6 +4961,7 @@ function ($value): ?string {
             ],
             'admission_mode',
             'check_plagiarism',
+            //'recommended', // Removed from auto-update as it's handled manually
path: frontend/server/src/Controllers/Contest.php
user: login: Avadhutgiri
body: Okay ,Sure.
created_at: 2025-04-06T14:21:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -757,7 +758,7 @@ groupNotFound = "Group not found"
 groupScoreboardDetailsCoder = "Coder"
 groupsCanNotBeAddedInContestForTeams = "Groups cannot be added in contests for teams"
 groupsCreateNew = "New group"
-groupsCsvHelp = "<p>Quickly add multiple identities to this group. Identities are not regular accounts, and don't need to have an email or validate their account.</p>\n<p>Identities should be in a <code>.csv</code> file with the following format:\n<samp><pre>username,name,country_id,state_id,gender,school_name\nusername-1,Developer Diana,MX,AGU,female,Best School\nusername-2,Danny Dev,MX,AGU,male,Best School</pre></samp></p>"
+groupsCsvHelp = "<p>Quickly add multiple identities to this group. Identities are not regular accounts, and don't need to have an email or validate their account.</p>\n<p>Identities should be in a <code>.csv</code> file with the following format:\n<samp><pre>username,name,country_id,state_id,gender,school_name\nteam-1,Team 1,MX,AGU,other,Best School\nteam-2,Teams 2,MX,AGU,decline,Best School\n```"
path: frontend/templates/en.lang
user: login: Avadhutgiri
body: Yeah, Sorry about that.
created_at: 2025-04-06T14:21:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -357,6 +355,23 @@
             </p>
           </div>
         </div>
+        <div class="row">
+          <div class="form-group col-md-6">
+            <label>{{ T.contestFilterByRecommended }}</label>
path: frontend/www/js/omegaup/components/contest/NewForm.vue
user: login: Avadhutgiri
body: Yes , My mistake.
created_at: 2025-04-06T14:21:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -357,6 +355,23 @@
             </p>
           </div>
         </div>
+        <div class="row">
+          <div class="form-group col-md-6">
+            <label>{{ T.contestFilterByRecommended }}</label>
+            <div v-if="canSetRecommended" class="checkbox form-check">
+              <input
+                v-model="recommended"
+                data-recommended
+                class="form-check-input"
+                type="checkbox"
+              />
+              <label class="form-check-label"> {{ T.wordsEnable }}</label>
+            </div>
+            <p class="help-block">
+              {{ canSetRecommended ? T.arenaPageRecommendedContestsTextAdmin : T.arenaPageRecommendedContestsTextNonAdmin }}
path: frontend/www/js/omegaup/components/contest/NewForm.vue
user: login: Avadhutgiri
body: Yes I will do it.

created_at: 2025-04-06T14:21:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4855,6 +4877,23 @@ public static function apiUpdate(\OmegaUp\Request $r): array {
 
         $updateProblemset = true;
         $updateRequests = false;
+        
+        // Handle recommended flag - only available for admins and support team
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: If in the following line you validate that the user is a sysadmin or a support team member, I don't see the need to add an intermediate variable. So it can be removed. It will throw an exception anyway for those who don't have permission to perform this action
created_at: 2025-04-06T17:31:33Z
author_association: COLLABORATOR
diff_hunk: @@ -4855,6 +4877,23 @@ public static function apiUpdate(\OmegaUp\Request $r): array {
 
         $updateProblemset = true;
         $updateRequests = false;
+        
+        // Handle recommended flag - only available for admins and support team
path: frontend/server/src/Controllers/Contest.php
user: login: Avadhutgiri
body: Okay Will do it

created_at: 2025-04-06T19:39:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1912,6 +1913,7 @@ function () use ($contest) {
                     'title',
                     'window_length',
                     'check_plagiarism',
+            //'recommended', // Removed from auto-update as it's handled manually
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: If this line is not needed, could you please delete it?
```suggestion
```
created_at: 2025-04-07T07:30:18Z
author_association: COLLABORATOR
diff_hunk: @@ -2671,6 +2675,18 @@ public static function apiCreate(\OmegaUp\Request $r) {
             ['partial','all_or_nothing','max_per_group'],
         );
         $checkPlagiarism = $r->ensureOptionalBool('check_plagiarism') ?? false;
+        
+        // Handle recommended flag - only available for admins and support team
+        if (
+            !is_null($r->ensureOptionalBool('recommended')) &&
+            !\OmegaUp\Authorization::isSystemAdmin($r->identity) &&
+            !\OmegaUp\Authorization::isSupportTeamMember($r->identity)
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: Sysadmins should have support team member role, so line 2682 can be deleted:
```suggestion
            !is_null($r->ensureOptionalBool('recommended')) &&
            !\OmegaUp\Authorization::isSupportTeamMember($r->identity)
```
created_at: 2025-04-07T07:32:40Z
author_association: COLLABORATOR
diff_hunk: @@ -2691,6 +2707,7 @@ public static function apiCreate(\OmegaUp\Request $r) {
             'show_scoreboard_after' => $r['show_scoreboard_after'] ?? true,
             'contest_for_teams' => $forTeams,
             'check_plagiarism' => $checkPlagiarism ? true : false,
+            'recommended' => $r->ensureOptionalBool('recommended'),
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: You are calling the same function twice. Could you please store the value in a variable and then use it in both places?
created_at: 2025-04-07T07:34:42Z
author_association: COLLABORATOR
diff_hunk: @@ -2791,9 +2809,9 @@ private static function validateCommonCreateOrUpdate(
         if (!empty($r['window_length'])) {
             $r->ensureOptionalInt(
                 'window_length',
+                0,
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: This doesn't seem to belong to the feature you're implementing, can it be reverted?
created_at: 2025-04-07T07:36:11Z
author_association: COLLABORATOR
diff_hunk: @@ -4855,6 +4875,21 @@ public static function apiUpdate(\OmegaUp\Request $r): array {
 
         $updateProblemset = true;
         $updateRequests = false;
+        
+        // Handle recommended flag - only available for admins and support team
+        if (
+            !is_null($r->ensureOptionalBool('recommended')) &&
+            !\OmegaUp\Authorization::isSystemAdmin($r->identity) &&
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: nit:
```suggestion
```
created_at: 2025-04-07T07:36:41Z
author_association: COLLABORATOR
diff_hunk: @@ -4855,6 +4875,21 @@ public static function apiUpdate(\OmegaUp\Request $r): array {
 
         $updateProblemset = true;
         $updateRequests = false;
+        
+        // Handle recommended flag - only available for admins and support team
+        if (
+            !is_null($r->ensureOptionalBool('recommended')) &&
+            !\OmegaUp\Authorization::isSystemAdmin($r->identity) &&
+            !\OmegaUp\Authorization::isSupportTeamMember($r->identity)
+        ) {
+            throw new \OmegaUp\Exceptions\ForbiddenAccessException(
+                'userNotAllowed'
+            );
+        }
+        
+        // Set the recommended value on the contest object
+        $contest->recommended = $r->ensureOptionalBool('recommended');
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: You are calling the same function twice. Could you please store the value in a variable and then use it in both places?
created_at: 2025-04-07T07:37:07Z
author_association: COLLABORATOR
diff_hunk: @@ -2724,6 +2740,7 @@ public static function apiCreate(\OmegaUp\Request $r) {
      * @omegaup-request-param 'contest_start'|'none'|'problem_open'|'runtime'|null $penalty_type
      * @omegaup-request-param float|null $points_decay_factor
      * @omegaup-request-param null|string $problems
+     * @omegaup-request-param bool|null $recommended
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: It seems like this line is not required:
```suggestion
```
created_at: 2025-04-07T19:30:18Z
author_association: COLLABORATOR
diff_hunk: @@ -2947,6 +2964,7 @@ private static function validateCommonCreateOrUpdate(
      * @omegaup-request-param 'contest_start'|'none'|'problem_open'|'runtime'|null $penalty_type
      * @omegaup-request-param float|null $points_decay_factor
      * @omegaup-request-param null|string $problems
+     * @omegaup-request-param bool|null $recommended
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: This one is not required either.
```suggestion
```
created_at: 2025-04-07T19:30:48Z
author_association: COLLABORATOR
diff_hunk: @@ -2985,6 +3003,7 @@ private static function validateCreate(
      * @omegaup-request-param 'contest_start'|'none'|'problem_open'|'runtime'|null $penalty_type
      * @omegaup-request-param float|null $points_decay_factor
      * @omegaup-request-param null|string $problems
+     * @omegaup-request-param bool|null $recommended
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: This one is not required either.
```suggestion
```
created_at: 2025-04-07T19:31:22Z
author_association: COLLABORATOR
diff_hunk: @@ -4855,6 +4874,21 @@ public static function apiUpdate(\OmegaUp\Request $r): array {
 
         $updateProblemset = true;
         $updateRequests = false;
+        
+        // Handle recommended flag - only available for admins and support team
+        $recommendedValue = $r->ensureOptionalBool('recommended');
+        if (
+            !is_null($recommendedValue) &&
+            !\OmegaUp\Authorization::isSupportTeamMember($r->identity)
+        ) {
+            throw new \OmegaUp\Exceptions\ForbiddenAccessException(
+                'userNotAllowed'
+            );
+        }
+        
+        // Set the recommended value on the contest object
+        $contest->recommended = $recommendedValue;
+        
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: Ok, what I was afraid of just happened:

It turns out that both in the contest creation endpoint and the contest editing endpoint, the client is sending the `recommended` parameter. So these validations never work because they will always throw an exception.

So there are two ways to fix it:

- Fix it from the Vue form, so that the `recommended` value is not sent when the user is not a sysadmin or support team member.

- Stop throwing the exception and only set the value when the conditions are met.

Due to time constraints, the second option might be simpler. It could be written like this:
```suggestion
        $recommendedValue = $r->ensureOptionalBool('recommended');
        if (
            !is_null($recommendedValue) &&
            \OmegaUp\Authorization::isSupportTeamMember($r->identity)
        ) {
            // Set the recommended value on the contest object
            $contest->recommended = $recommendedValue;
        }
```
created_at: 2025-04-07T19:33:46Z
author_association: COLLABORATOR
diff_hunk: @@ -4855,6 +4874,21 @@ public static function apiUpdate(\OmegaUp\Request $r): array {
 
         $updateProblemset = true;
         $updateRequests = false;
+        
+        // Handle recommended flag - only available for admins and support team
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: In this function, you would need to add the `request-param`, because this is where you're getting its value.
```php
     * @omegaup-request-param bool|null $recommended
```

created_at: 2025-04-07T19:35:12Z
author_association: COLLABORATOR
diff_hunk: @@ -2671,6 +2674,18 @@ public static function apiCreate(\OmegaUp\Request $r) {
             ['partial','all_or_nothing','max_per_group'],
         );
         $checkPlagiarism = $r->ensureOptionalBool('check_plagiarism') ?? false;
+        
+        // Handle recommended flag - only available for admins and support team
+        $recommendedValue = $r->ensureOptionalBool('recommended');
+        if (
+            !is_null($recommendedValue) &&
+            !\OmegaUp\Authorization::isSupportTeamMember($r->identity)
+        ) {
+            throw new \OmegaUp\Exceptions\ForbiddenAccessException(
+                'userNotAllowed'
+            );
+        }
+        
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: Here you can use the exact same code as in the `apiUpdate` function:
```suggestion
        $recommendedValue = $r->ensureOptionalBool('recommended');
        if (
            !is_null($recommendedValue) &&
            \OmegaUp\Authorization::isSupportTeamMember($r->identity)
        ) {
            // Set the recommended value on the contest object
            $contest->recommended = $recommendedValue;
        }
```
But for it to work, you would need to move it below the creation of the `Contests` object.
created_at: 2025-04-07T19:36:32Z
author_association: COLLABORATOR
diff_hunk: @@ -2691,6 +2706,7 @@ public static function apiCreate(\OmegaUp\Request $r) {
             'show_scoreboard_after' => $r['show_scoreboard_after'] ?? true,
             'contest_for_teams' => $forTeams,
             'check_plagiarism' => $checkPlagiarism ? true : false,
+            'recommended' => $recommendedValue,
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: With the change from the previous comment, this would no longer be needed:
```suggestion
```
created_at: 2025-04-07T19:37:00Z
author_association: COLLABORATOR
diff_hunk: @@ -5331,6 +5365,8 @@ public static function getStatsDataForTypeScript(\OmegaUp\Request $r) {
 
     /**
      * @return array{entrypoint: string, templateProperties: array{payload: ScoreboardMergePayload, title: \OmegaUp\TranslationString}}
+     *
+     * @omegaup-request-param null|string $contest_alias
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: It seems like this change doesn't belong in this PR.
```suggestion
```
created_at: 2025-04-07T19:37:32Z
author_association: COLLABORATOR
diff_hunk: @@ -73,6 +73,8 @@ const translations: { [key: string]: string; } = {
   arenaCourseAssignmentWillBeginIn: "This content will begin in %(time)",
   arenaCourseProblemTitle: "%(letter). %(title)",
   arenaPageRecommendedContestsText: "To show your contest in the Recommended Contests list, contact soporte@omegaup.com.",
+  arenaPageRecommendedContestsTextAdmin: "Enable this option if you want your contest to appear in the Recommended Contests list.",
+  arenaPageRecommendedContestsTextNonAdmin: "To show your contest in the Recommended Contests list, contact soporte@omegaup.com.",
path: frontend/www/js/omegaup/lang.en.ts
user: login: pabo99
body: These two translation strings are no longer used. Can they be removed?
```suggestion
```

The same applies to the other translation files.
created_at: 2025-04-07T19:38:41Z
author_association: COLLABORATOR
diff_hunk: @@ -2089,6 +2091,9 @@ const translations: { [key: string]: string; } = {
   wordsWarningProblem: "Keep with a warning",
   wordsYes: "Yes",
   zipPrepare: "Prepare zip file",
+  contestNewFormRecommended: "Recommended Contest",
+  contestNewFormRecommendedTextAdmin: "Enable this option if you want your contest to appear in the Recommended Contests list.",
+  contestNewFormRecommendedTextNonAdmin: "To show your contest in the Recommended Contests list, contact soporte@omegaup.com.",
path: frontend/www/js/omegaup/lang.en.ts
user: login: pabo99
body: I was having a hard time figuring out where the translations were coming from, and it turns out I found the error you're seeing in the tests.

The texts are well written in my opinion.

The problem is that they're not being added in the correct place. The correct files are:
- `frontend/templates/en.lang`
- `frontend/templates/es.lang`
- `frontend/templates/pt.lang`

After that, you need to run the command:
```
./stuff/lint.sh
```
That will help propagate the changes to these files automatically.
created_at: 2025-04-07T19:40:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,5 @@
+declare namespace JSX {
+  interface IntrinsicElements {
+    [elem: string]: any;
+  }
+} 
path: frontend/www/js/omegaup/vue-html.d.ts
user: login: pabo99
body: This works to eliminate the errors in the HTML tags used in the templates, right?

If you’d like, you can move this change to a separate PR and it can be approved instantly.
created_at: 2025-04-07T19:40:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,5 @@
+declare namespace JSX {
+  interface IntrinsicElements {
+    [elem: string]: any;
+  }
+} 
path: frontend/www/js/omegaup/vue-html.d.ts
user: login: Avadhutgiri
body: Okay Will move this in different PR
created_at: 2025-04-07T20:30:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2671,6 +2678,9 @@ public static function apiCreate(\OmegaUp\Request $r) {
             ['partial','all_or_nothing','max_per_group'],
         );
         $checkPlagiarism = $r->ensureOptionalBool('check_plagiarism') ?? false;
+
+        // Handle recommended flag - only available for admins and support team
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: In this function, you would need to add the `request-param`, because this is where you're getting its value.
```php
     * @omegaup-request-param bool|null $recommended
```

created_at: 2025-04-10T15:20:21Z
author_association: COLLABORATOR
diff_hunk: @@ -5331,6 +5365,8 @@ public static function getStatsDataForTypeScript(\OmegaUp\Request $r) {
 
     /**
      * @return array{entrypoint: string, templateProperties: array{payload: ScoreboardMergePayload, title: \OmegaUp\TranslationString}}
+     *
+     * @omegaup-request-param null|string $contest_alias
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: This change is still unattended
created_at: 2025-04-10T15:21:42Z
author_association: COLLABORATOR
diff_hunk: @@ -1379,6 +1379,9 @@ public static function getContestNewForTypeScript(
                     'hasVisitedSection' => \OmegaUp\UITools::hasVisitedSection(
                         'has-visited-create-contest'
                     ),
+                    'canSetRecommended' => \OmegaUp\Authorization::isSupportTeamMember(
+                        $r->identity
+                    ),
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: It looks like we forgot to add this new parameter to the `ContestNewPayload` declaration. If we don’t add it, `TypeScript` won’t know it exists and will start throwing errors when it’s used on the client side
created_at: 2025-04-11T18:54:58Z
author_association: COLLABORATOR
diff_hunk: @@ -2139,6 +2142,10 @@ public static function getContestDetailsForAdmin(
         $result['admin'] = true;
         $result['scoreboard_url'] = $problemset->scoreboard_url;
         $result['scoreboard_url_admin'] = $problemset->scoreboard_url_admin;
+        $result['recommended'] = $contest->recommended;
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: This field is indeed part of the `Contests` object, so it should be retrieved from the cached details. I remember you had it that way in a previous revision — I must have missed it when you removed it.

After applying the change, the new type should be added in the appropriate places
created_at: 2025-04-11T19:02:07Z
author_association: COLLABORATOR
diff_hunk: @@ -2139,6 +2142,10 @@ public static function getContestDetailsForAdmin(
         $result['admin'] = true;
         $result['scoreboard_url'] = $problemset->scoreboard_url;
         $result['scoreboard_url_admin'] = $problemset->scoreboard_url_admin;
+        $result['recommended'] = $contest->recommended;
+        $result['canSetRecommended'] = \OmegaUp\Authorization::isSupportTeamMember(
+            $adminIdentity
+        );
path: frontend/server/src/Controllers/Contest.php
user: login: pabo99
body: This field will also need to be added to the `ContestAdminDetails` type, since it is being included here and we also forgot to add it in previous revisions.
created_at: 2025-04-11T19:22:06Z
author_association: COLLABORATOR
diff_hunk: @@ -2693,6 +2707,13 @@ public static function apiCreate(\OmegaUp\Request $r) {
             'check_plagiarism' => $checkPlagiarism ? true : false,
         ]);
 
+        if (
+            !is_null($recommendedValue) &&
+            \OmegaUp\Authorization::isSupportTeamMember($r->identity)
path: frontend/server/src/Controllers/Contest.php
user: login: heduenas
body: if an unauthorized user is trying to set this vlaue we should throw an error instead of just skipping setting that value. Also, please add a few test cases for the changes. Make sure to add a test for the case where the user is not a support team member.
created_at: 2025-04-20T19:44:24Z
author_association: MEMBER
diff_hunk: @@ -2693,6 +2707,13 @@ public static function apiCreate(\OmegaUp\Request $r) {
             'check_plagiarism' => $checkPlagiarism ? true : false,
         ]);
 
+        if (
+            !is_null($recommendedValue) &&
+            \OmegaUp\Authorization::isSupportTeamMember($r->identity)
path: frontend/server/src/Controllers/Contest.php
user: login: Avadhutgiri
body: Hey @heduenas , 
here in frontend I have implemented such that  if user is unauthorized then he wont be able to see the checkbox 
![Screenshot from 2025-04-21 16-22-41
](https://github.com/user-attachments/assets/ece7e23b-6ba1-4afd-b256-31da418d07cd)

created_at: 2025-04-21T10:54:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2693,6 +2707,13 @@ public static function apiCreate(\OmegaUp\Request $r) {
             'check_plagiarism' => $checkPlagiarism ? true : false,
         ]);
 
+        if (
+            !is_null($recommendedValue) &&
+            \OmegaUp\Authorization::isSupportTeamMember($r->identity)
path: frontend/server/src/Controllers/Contest.php
user: login: Avadhutgiri
body: Still  I can update it if needed

created_at: 2025-04-21T10:54:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2693,6 +2707,13 @@ public static function apiCreate(\OmegaUp\Request $r) {
             'check_plagiarism' => $checkPlagiarism ? true : false,
         ]);
 
+        if (
+            !is_null($recommendedValue) &&
+            \OmegaUp\Authorization::isSupportTeamMember($r->identity)
path: frontend/server/src/Controllers/Contest.php
user: login: Avadhutgiri
body: I have added the throw with message as UserNotAllowed if unauthorized user tries to change the recommended checkbox also Have added the 4 tests 
Create and Update Contest for Admin or Support User
Create and Update Contest for Non-support User
created_at: 2025-04-21T13:34:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: Avadhutgiri
created_at: 2025-04-05T01:42:35Z
body: Sure I will update them

author_association: CONTRIBUTOR
user: login: Avadhutgiri
created_at: 2025-04-05T08:00:17Z
body: I have made the changes Can you review it ?
Thanks

author_association: CONTRIBUTOR
user: login: Avadhutgiri
created_at: 2025-04-06T14:58:33Z
body: I hope the changes align with your expectations, @pabo99.
Sorry for any inconvenience caused, and thanks again for your patience!
author_association: CONTRIBUTOR
user: login: Avadhutgiri
created_at: 2025-04-07T08:24:53Z
body: Hey @pabo99  I have updated as per your instructions .
Sorry for any inconvenience caused, and thanks again for your patience !!
author_association: CONTRIBUTOR
user: login: Avadhutgiri
created_at: 2025-04-07T17:14:18Z
body: Hey @pabo99  so Do I need to solve this from my side ?


author_association: CONTRIBUTOR
user: login: pabo99
created_at: 2025-04-07T20:15:35Z
body: It seems like you only addressed the suggestions related to the translation files. There are still some comments from my last review that haven't been addressed yet, which will help fix the PHP/PSALM errors.
author_association: COLLABORATOR
user: login: Avadhutgiri
created_at: 2025-04-07T20:18:03Z
body: Yes I just saw your review now Sorry my bad

author_association: CONTRIBUTOR
user: login: Avadhutgiri
created_at: 2025-04-07T21:05:40Z
body: I have made all the changes that were mentioned

author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-18T16:48:56Z
body: ## [Codecov](https://app.codecov.io/gh/omegaup/omegaup/pull/8147?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omegaup) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 82.24%. Comparing base [(`0ee3cfb`)](https://app.codecov.io/gh/omegaup/omegaup/commit/0ee3cfbcd73c71e1c452ee0d2fa64c9d59042821?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omegaup) to head [(`6e7374a`)](https://app.codecov.io/gh/omegaup/omegaup/commit/6e7374ab056c95fa09692c0ae0f2ec2d2d0a3e63?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omegaup).
> Report is 2 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##               main    #8147      +/-   ##
============================================
+ Coverage     77.83%   82.24%   +4.41%     
============================================
  Files           153      232      +79     
  Lines         31775     5368   -26407     
  Branches          0      684     +684     
============================================
- Hits          24732     4415   -20317     
+ Misses         7043      952    -6091     
- Partials          0        1       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omegaup/omegaup/pull/8147?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omegaup).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omegaup).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: pabo99
created_at: 2025-04-19T03:12:33Z
body: The latest comments haven't been addressed. Do you need help resolving them?
author_association: COLLABORATOR
user: login: Avadhutgiri
created_at: 2025-04-20T02:17:50Z
body: Hey @pabo99 ,Sorry It was my exam week I will try to push the changes today
author_association: CONTRIBUTOR
user: login: Avadhutgiri
created_at: 2025-04-20T18:29:31Z
body: hey @heduenas ,
can you review it again Ig this is done ??

author_association: CONTRIBUTOR
user: login: Avadhutgiri
created_at: 2025-04-21T13:26:51Z
body: Hey @heduenas , I have updated the code according to the changes specified by you.
Just do I need to check why cypress is failing and should I make changes in it ??

author_association: CONTRIBUTOR
user: login: Avadhutgiri
created_at: 2025-04-22T17:54:40Z
body: hey @heduenas as per your requests I have added 5 tests in ContestRecommendedTests  /frontend/tests/controllers/
Can You review it once again.
author_association: CONTRIBUTOR
user: login: pabo99
created_at: 2025-04-25T06:52:10Z
body: I’ve reviewed the PR again and everything seems to be working correctly. Also, the tests will help prevent regressions in the future.

@Avadhutgiri: Could you add a screencast in the PR description to show how it works from the Administrator account?

@heduenas: Could you check if there’s anything else missing so we can approve it and move it to production?
author_association: COLLABORATOR
user: login: Avadhutgiri
created_at: 2025-04-25T12:55:43Z
body: @pabo99 I have added the video as administrator and user view
author_association: CONTRIBUTOR
user: login: pabo99
created_at: 2025-04-25T15:34:17Z
body: > @pabo99 I have added the video as administrator and user view

Excellent video! You covered all the edge cases! Thank you, @Avadhutgiri !
author_association: COLLABORATOR
user: login: Avadhutgiri
created_at: 2025-04-26T13:25:31Z
body: Welcome @pabo99 !!
Hope This PR gets merged early!!
author_association: CONTRIBUTOR