Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I1030 WTI clar announcement filtering #1041

Open
wants to merge 42 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
67ca24a
i1030: remove redundant "Recipient" column from clars page.
clevengr Jan 15, 2025
1f10068
i1030: set announcement clar state for empty OR "announce" question:
clevengr Jan 15, 2025
86c6d99
i1030: remove test for "isJudge" in deciding clars to send to team.
clevengr Jan 15, 2025
94cb267
i1030: set announcement clarification question text to "Announcement"
clevengr Jan 15, 2025
3fd07e0
i1030: add doc, refactor some names, update for announcements.
Jan 21, 2025
7408147
i1030: add/update doc; refactor some names (no logic changes).
Jan 21, 2025
c4e1a7a
i1030: update the way an intended clar recipient is specified.
Jan 21, 2025
d8b2f1d
i1030: update doc to clarify what clars are returned. No logic changes.
Jan 21, 2025
6c2e8f6
i1030: replace combobox ItemListener with ActionListener, because...
Jan 21, 2025
b143c88
i1030: change WTI-UI button label from "Just My Team" to "My Team".
Jan 27, 2025
21fbc1f
i1030: initial pass at separating Groups/Teams scrollpanes.
Jan 27, 2025
34d342f
i1030: replace SubmClarPane with GenerateAnnouncementPane.
clevengr Jan 30, 2025
6d0d2f4
i1030: initial version of new GenerateAnnouncementPane.
clevengr Jan 30, 2025
2b88bc0
i1030: minor additions to SubmiClarPane. However:
clevengr Jan 30, 2025
7860e69
i1030: add SubmitClarificationPane to DeveloperPane. Note:
clevengr Jan 30, 2025
036b9c6
i1030: add GenerateAnnouncements pane to Admin View.
clevengr Jan 30, 2025
f961c79
i1030: minor renaming and unnecessary code removal.
clevengr Jan 31, 2025
a6db017
i1030: rename tabs on JudgeView.
Jan 31, 2025
f725cfd
i1030: remove unnecessary (commented-out) code.
Jan 31, 2025
bdc7a61
i1030: implement getGroupsAndTeamsSelectedValues()
Jan 31, 2025
3e3bb6d
i1030: change tab labels from "Announcements" to "Announce".
Jan 31, 2025
b8e38a8
i1030: update logic to handle simultaneous groups and teams.
Jan 31, 2025
467b597
i1030: reverted SubmitClarificationPane to earlier version, because...
Jan 31, 2025
83f0656
i1030: remove debugging prints.
Feb 1, 2025
b311238
i1030: add GroupListener; update AccountListener.
Feb 1, 2025
6e8d577
i1030: update Group and Team change listeners; also...
Feb 2, 2025
50f3551
i1030: add getContestStartTime and toString to ContestTimeImpl; note:
Feb 2, 2025
e116b35
i1030: add getContestStartTime() to interface IContestClock. Note:
Feb 2, 2025
d9bbc48
i1030: import WTI-API ContestClockModel from branch i1027.
Feb 2, 2025
abd6be2
i1030: add /contestClock endpoint. Note that this also includes...
Feb 2, 2025
f069ec7
i1030: minor variable renaming for clarity; no logic changes.
Feb 2, 2025
3b70710
i1030: add destination teams and groups lists to ClarImpl.
Feb 3, 2025
08c748f
i1030: add clar destination teams & groups to WTI ClarificationModel.
Feb 3, 2025
efbc78c
i1030: remove (comment-out) WTI-UI clar filter-by-recipient buttons.
Feb 3, 2025
fbc133e
i1030: remove (comment-out) filtering of clars.
Feb 3, 2025
c2175d4
i1030: insert team and group destinations into clars sent to WTI-UI.
Feb 3, 2025
e5dd75c
i1030: restore filter buttons on WTI-UI Clarifications page.
Feb 4, 2025
1c31bc1
i1030: restore clar filtering; include "not answered" in all displays.
Feb 4, 2025
5a5dc72
i1030: support displaying "not answered" clars in WTI-UI.
Feb 4, 2025
a0637f0
i1030: clear selected groups/teams when switching to "All Teams" so...
Feb 4, 2025
c03f1b3
i1030: fix bug in getAllDestinationGroups/Teams(). Specifically...
clevengr Feb 6, 2025
c5f33c9
i1030: update copyright date in shared footer.
clevengr Feb 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 182 additions & 36 deletions projects/WTI-API/src/main/controllers/ContestController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
Expand All @@ -24,7 +25,8 @@
import communication.WTIWebsocket;
import config.ServerInit;
import edu.csus.ecs.pc2.api.IClarification;
import edu.csus.ecs.pc2.api.IClient;
import edu.csus.ecs.pc2.api.IContest;
import edu.csus.ecs.pc2.api.IContestClock;
import edu.csus.ecs.pc2.api.IJudgement;
import edu.csus.ecs.pc2.api.ILanguage;
import edu.csus.ecs.pc2.api.IProblem;
Expand All @@ -37,6 +39,7 @@
import edu.csus.ecs.pc2.core.log.Log;
import edu.csus.ecs.pc2.core.model.ClientId;
import edu.csus.ecs.pc2.core.model.ClientType.Type;
import edu.csus.ecs.pc2.core.model.ElementId;
import edu.csus.ecs.pc2.core.model.IInternalContest;
import edu.csus.ecs.pc2.core.model.Run;
import edu.csus.ecs.pc2.core.scoring.DefaultScoringAlgorithm;
Expand All @@ -48,6 +51,7 @@
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.Authorization;
import models.ClarificationModel;
import models.ContestClockModel;
import models.LanguageModel;
import models.ProblemModel;
import models.ServerErrorResponseModel;
Expand Down Expand Up @@ -325,16 +329,23 @@ public Response problems(
if (userInformation == null) {
throw new NotLoggedInException();
} else {
// make sure that the contest is running (problems are not allowed to be seen when the contest is not running)
if (!userInformation.getContest().isContestClockRunning()) {
// make sure that the contest has been started (problems are not allowed to be seen before the contest starts)
// Note that starting the contest results in setting the "contest start time" in the ContestClock object to
// the Unix Epoch time at which the contest was started; prior to starting the contest that value will be null.
if (userInformation.getContest().getContestClock().getContestStartTime()==null) {
return Response.status(Response.Status.UNAUTHORIZED).entity(
new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request"))
new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request - contest has not started."))
.type(MediaType.APPLICATION_JSON).build();
}
}
} catch (NotLoggedInException e1) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request"))
.entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request -- not logged in."))
.type(MediaType.APPLICATION_JSON).build();
} catch (Exception e2) {
logger.severe("Exception in ContestController /problems endppoint: " + e2.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "Exception in ContestController /problems endppoint: " + e2.getMessage()))
.type(MediaType.APPLICATION_JSON).build();
}

Expand Down Expand Up @@ -411,12 +422,114 @@ public Response isRunning(
.type(MediaType.APPLICATION_JSON).build();
}


/***
* This method returns a "ContestClock" object encapsulating the current state of the PC2 contest clock
* as maintained by the PC2 class "ContestTime".
* It first checks to see that the user (team) specified by the received "key" is currently logged in. If so, the method
* returns the current contest time information, obtained from the PC2 {@link ServerConnection} for the team.
*
* @param key a String containing a key which uniquely identifies the team making the request.
* The value of "key" is obtained from the HTTP header parameter "team_id".
*
* @return Response of:
* 401 (unauthorized) if team's credentials are incorrect (i.e. the team is not logged in or is not allowed to make such a request);
* 500 (INTERNAL_SERVER_ERROR) if an error occurs in fetching the requested data from the PC2 server;
* otherwise 200 (OK) and a JSON string containing a {@link ContestClock} is returned.
*/
@Path("/contestclock")
@GET
@ApiOperation(value = "contestclock",
notes = "Gets the PC2 contest clock (time info).")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns a contest clock object.", response = ContestClockModel.class),
@ApiResponse(code = 401, message = "Returned if invalid credentials are supplied", response = ServerErrorResponseModel.class)
})

public Response contestClock(@ApiParam(value="token used by logged in users to access teams information",
required = true) @HeaderParam("team_id")String key) {

if (connections == null) {
System.err.println ("SEVERE: ContestController.contestClock(): team connections map in MainController is null!");
logger.severe("ContestController.contestClock(): team connections map in MainController is null!");
throw new NullPointerException("Team connections map in MainController is null in ContestController.contestClock()");
}
ServerConnection userInformation = connections.get(key);

//verify the user is logged in
try {
// make sure we have connection information for this user (i.e. that the user is logged in)
if (userInformation == null) {
throw new NotLoggedInException();
}
} catch (NotLoggedInException e1) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request"))
.type(MediaType.APPLICATION_JSON).build();
}

//the ContestClock object to be returned in the response to the HTTP request
ContestClockModel returnableContestClock ;

try {
//get the contest clock from the PC2 Server via the PC2 API ServerConnection
// IContestClock contestClock = userInformation.getContest().getContestClock();

IContest contest = userInformation.getContest();
if (contest == null) {
System.err.println ("SEVERE: ContestController.contestClock(): ServerConnection.getContest() returned null!");
logger.severe("ContestController.contestClock(): ServerConnection.getContest() returned null!");
throw new NullPointerException("ServerConnection.getContest() returned null in ContestController.contestClock()");
}
IContestClock contestClock = contest.getContestClock();
if (contestClock == null) {
System.err.println ("SEVERE: ContestController.contestClock(): ServerConnection.getContest().getContestClock() returned null!");
logger.severe("ContestController.contestClock(): ServerConnection.getContest().getContestClock() returned null!");
throw new NullPointerException("ServerConnection.getContest().getContestClock() returned null in ContestController.contestClock()");
}

//retrieve the relevant fields from the PC2 contest clock
boolean isRunning = contestClock.isContestClockRunning();
long contestLengthInSecs = contestClock.getContestLengthSecs();
long elapsedSecs = contestClock.getElapsedSecs();
long wallClockStartTime ;
//"contest start time" is a Calendar object and might be null if the contest has never been started
Calendar startTimeCalendar = contestClock.getContestStartTime();
if (startTimeCalendar == null) {
wallClockStartTime = 0;
} else {
wallClockStartTime = startTimeCalendar.getTimeInMillis();
}

//construct a ContestClock containing the PC2 Server clock values
returnableContestClock = new ContestClockModel(isRunning,contestLengthInSecs, elapsedSecs, wallClockStartTime);

}
catch(NotLoggedInException e) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request"))
.type(MediaType.APPLICATION_JSON).build();
}
catch(NullPointerException e) {
logger.severe(e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "NullPointerException in ContestController.contestClock()"))
.type(MediaType.APPLICATION_JSON).build();
}

//return the PC2 ContestClock, mapped into JSON
return Response.ok()
.entity(returnableContestClock)
.type(MediaType.APPLICATION_JSON).build();
}

/***
* This method returns a list of all the clarifications in the PC^2 contest submitted by the specified team.
* It first checks to see that the user (team) specified by the received "key" is currently logged in and that the
* contest is running (contest clarifications are only allowed to be viewed when the contest is running). If so, the method
* This method returns a list of all the clarifications in the PC^2 contest which the requesting client is allowed to see.
* It first checks to see that the user (team) specified by the received "key" is currently logged in. If so, the method
* returns the current team's clarifications, obtained from the PC2 {@link ServerConnection} for the team.
*
* Note that only clarifications which are allowed to be viewed by the requesting client are returned.
*
* @param key a String containing a key which uniquely identifies the team making the request.
* The value of "key" is obtained from the HTTP header parameter "team_id".
*
Expand All @@ -439,22 +552,15 @@ public Response clarifications(@ApiParam(value="token used by logged in users to

ServerConnection userInformation = connections.get(key);

//verify the user is logged in and the contest is running
//verify the user is logged in
try {
// make sure we have connection information for this user (i.e. that the user is logged in)
if (userInformation == null) {
throw new NotLoggedInException();
} else {
// make sure that the contest is running (problems are not allowed to be seen when the contest is not running)
if (!userInformation.getContest().isContestClockRunning()) {
return Response.status(Response.Status.UNAUTHORIZED).entity(
new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request"))
.type(MediaType.APPLICATION_JSON).build();
}
}
} catch (NotLoggedInException e1) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request"))
.entity(new ServerErrorResponseModel(Response.Status.UNAUTHORIZED, "Unauthorized user request - not logged in."))
.type(MediaType.APPLICATION_JSON).build();
}

Expand All @@ -463,30 +569,63 @@ public Response clarifications(@ApiParam(value="token used by logged in users to

try {

//This function only gets clarifications that the current Client should receive.
//get the clarifications associated with the invoking client's Id.
//Note that getClarificationsWithClientId() returns exactly, and only, clarifications that the current Client should receive.
//This includes clars that were submitted by the team as well as announcement clars directed to the team -- either
//specifically or as a result of being directed to "All Teams" or to a Group of which the team is a member.
IClarification[] clars = userInformation.getContest().getClarificationsWithClientId();

//check all contest clars to see which should be returned to the team
//encode each PC2 clar into a WTI ClarificationModel
for(IClarification clar : clars) {

boolean isJudge = clar.getTeam().getType() == IClient.ClientType.JUDGE_CLIENT;

//return to the team only those clars that came from the team, or from the judges
if(clar.getTeam().getLoginName().equalsIgnoreCase(userInformation.getMyClient().getLoginName()) || isJudge || clar.isSendToAll()) {
//TODO must also include clars directed to here from group

String displayName = (isJudge || clar.isSendToAll()) ? "All" : clar.getTeam().getDisplayName();
clarifications.add(new ClarificationModel(
displayName,
clar.getProblem().getName(),
clar.getQuestion(),
clar.getAnswer(),
String.format("%s-%s", clar.getSiteNumber(), clar.getNumber()),
clar.getSubmissionTime(),
clar.isAnswered()));
//holders for the lists of recipient teams and groups (if any)
ArrayList<String> teamRecipientList = new ArrayList<String>();
ArrayList<String> groupRecipientList = new ArrayList<String>();

//find the teams (if any) and groups (if any) to whom the answer to this clar (if any) was sent
if (clar.isAnswered()) {

//add recipient team numbers to list
ArrayList<ClientId> teamRecipientIds = clar.getDestinationTeams();
for (ClientId client : teamRecipientIds) {
teamRecipientList.add(Integer.toString(client.getClientNumber()));
}
// add recipient group NAMES to list (the group "number" isn't useful/meaningful since it's just an internal PC2 ElementId)
ArrayList<ElementId> groupRecipientIds = clar.getDestinationGroups();
for (ElementId groupId : groupRecipientIds) {
String groupName = userInformation.getContest().getInternalContest().getGroup(groupId).getDisplayName();
groupRecipientList.add(groupName);
}
}

// set the "recipient type" indicating whether this clar goes to the team because it was "sent to all",
// because it was sent to several teams including this team or a group containing this team,
// or it was sent only to this team. (The "recipient type" is used by the WTI-UI
// to determine under what conditions to display the clar on the Clarifications page.)
String recipientType = "";
if (clar.isAnswered()) {
if (clar.isSendToAll()) {
recipientType = "All";
} else if (teamRecipientList.size() > 1 || !groupRecipientList.isEmpty()) {
recipientType = "Some";
} else {
recipientType = "My Team";
}
} else {
//clar hasn't been answered yet
recipientType = "No Answer Yet";
}
clarifications.add(new ClarificationModel(
recipientType,
teamRecipientList,
groupRecipientList,
clar.getProblem().getName(),
clar.getQuestion(),
clar.getAnswer(),
String.format("%s-%s", clar.getSiteNumber(), clar.getNumber()), //notional "id" for the clar
clar.getSubmissionTime(),
clar.isAnswered()));
}

}
catch(NotLoggedInException e) {
return Response.status(Response.Status.UNAUTHORIZED)
Expand All @@ -498,7 +637,14 @@ public Response clarifications(@ApiParam(value="token used by logged in users to
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "NullPointerException in ContestController.clarifications()"))
.type(MediaType.APPLICATION_JSON).build();
}
}
catch(Exception e) {
logger.severe(e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ServerErrorResponseModel(Response.Status.INTERNAL_SERVER_ERROR, "Exception in ContestController.clarifications(): " + e.getMessage()))
.type(MediaType.APPLICATION_JSON).build();
}


return Response.ok()
.entity(clarifications)
Expand Down
23 changes: 11 additions & 12 deletions projects/WTI-API/src/main/models/ClarificationModel.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
package models;

import java.util.ArrayList;

/**
* This class encapsulates the WTI view of a PC2 clarification (which includes "announcement clarifications").
*
*/
public class ClarificationModel {

public String recipient, problem, question, answer;
public ArrayList<String> teamRecipients, groupRecipients;
public String id;
public long time;
public boolean isAnswered;

public ClarificationModel(String recipient, String problem, String question, String answer, long time, boolean isAnswered) {
this.recipient = recipient;
this.problem = problem;
this.question = question;
this.answer = answer;
this.time = time;
this.isAnswered = isAnswered;

}

public ClarificationModel(String recipient, String problem, String question, String answer, String id, long time,
boolean isAnswered) {
public ClarificationModel(String recipient, ArrayList<String> teamRecipients, ArrayList<String> groupRecipients,
String problem, String question, String answer, String id, long time, boolean isAnswered) {
super();
this.recipient = recipient;
this.teamRecipients = teamRecipients;
this.groupRecipients = groupRecipients;
this.problem = problem;
this.question = question;
this.answer = answer;
Expand Down
45 changes: 45 additions & 0 deletions projects/WTI-API/src/main/models/ContestClockModel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package models;

import edu.csus.ecs.pc2.core.model.ContestTime;

/**
* This class encapsulates the features of the PC2 "contest clock" (class {@link ContestTime}) which need to be able to be passed
* to the WTI-UI.
*
* @author JohnC
*
*/
public class ContestClockModel {

private boolean isRunning;
private long contestLengthInSecs;
private long elapsedSecs; //total time in seconds that the contest has been running -- does NOT include time during any "pauses"
private long wallClockStartTime; //unix timestamp when the contest actually started (msec since the Epoch),
// or zero if contest has not ever been started. Does not change due to "pauses".

public ContestClockModel(boolean isRunning, long contestLengthInSecs, long elapsedSecs, long wallClockStartTime) {
this.isRunning = isRunning;
this.contestLengthInSecs = contestLengthInSecs;
this.elapsedSecs = elapsedSecs;
this.wallClockStartTime = wallClockStartTime;
}

public ContestClockModel() { }

public boolean isRunning() {
return isRunning;
}

public long getContestLengthInSecs() {
return contestLengthInSecs;
}

public long getElapsedSecs() {
return elapsedSecs;
}

public long getWallClockStartTime() {
return wallClockStartTime;
}
}

Loading