Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
94f4c45
create endpoint to query programming exercise by repo identifier
janthoXO Apr 20, 2025
cdd3569
fix server tests with wrong url
janthoXO Apr 20, 2025
994c555
change dto name to use the correct term repo name
janthoXO Apr 21, 2025
152bdb1
use optional nullable for coursedto creation
janthoXO Apr 21, 2025
f918b61
adjust formating to fulfil code style
janthoXO Apr 23, 2025
01dc5d6
Merge branch 'develop' of github.com:ls1intum/Artemis into feature/pr…
janthoXO Apr 23, 2025
73c2934
Merge branch 'develop' into feature/programming-exercise-by-repo-iden…
janthoXO Apr 25, 2025
0b67324
make dto more lightweight by using singleton empty collections
janthoXO Apr 27, 2025
898cb9f
Merge branch 'develop' into feature/programming-exercise-by-repo-iden…
janthoXO Apr 27, 2025
ea0cda1
Merge branch 'develop' into feature/programming-exercise-by-repo-iden…
janthoXO May 5, 2025
7302333
Merge branch 'develop' into feature/programming-exercise-by-repo-iden…
krusche May 10, 2025
1ea181b
Merge branch 'develop' into feature/programming-exercise-by-repo-iden…
janthoXO May 14, 2025
2dc5762
merge
janthoXO Jun 7, 2025
a284981
Merge branch 'feature/programming-exercise-by-repo-identifier' of git…
janthoXO Jun 7, 2025
0b8561e
let endpoint only return participation. results are now fetched by sc…
janthoXO Jun 7, 2025
fc131b2
Update src/main/java/de/tum/cit/aet/artemis/programming/web/Programmi…
janthoXO Jun 7, 2025
7a33638
rename to repoUri
janthoXO Jun 7, 2025
5f5ea08
Merge branch 'develop' into feature/programming-exercise-by-repo-iden…
janthoXO Jun 7, 2025
b9b8287
allow repository url as well as name + adjust tests
janthoXO Jun 7, 2025
34eb574
Merge branch 'feature/programming-exercise-by-repo-identifier' of git…
janthoXO Jun 7, 2025
ec6f743
remove repoUri param
janthoXO Jun 12, 2025
b8ade7a
Update src/main/java/de/tum/cit/aet/artemis/programming/domain/VcsRep…
janthoXO Jun 12, 2025
3a0dbc9
adjust dto doc comment
janthoXO Jun 12, 2025
204ef6c
change vcUrl to localVCBaseUrl
janthoXO Jun 12, 2025
494d242
Merge branch 'feature/programming-exercise-by-repo-identifier' of git…
janthoXO Jun 12, 2025
3b20ad1
Merge branch 'develop' into feature/programming-exercise-by-repo-iden…
janthoXO Jun 13, 2025
a961b04
merge
janthoXO Jun 15, 2025
58ca62d
Merge branch 'develop' into feature/programming-exercise-by-repo-iden…
janthoXO Jun 16, 2025
4de266d
Merge branch 'develop' into feature/programming-exercise-by-repo-iden…
janthoXO Jun 22, 2025
c4b37f2
merge
janthoXO Jun 29, 2025
e12ed75
Merge branch 'feature/programming-exercise-by-repo-identifier' of git…
janthoXO Jun 29, 2025
0a86125
Merge branch 'develop' into feature/programming-exercise-by-repo-iden…
Mtze Jul 7, 2025
6f6678a
adjusst param required flag
janthoXO Jul 8, 2025
c07b93a
Merge branch 'feature/programming-exercise-by-repo-identifier' of git…
janthoXO Jul 8, 2025
113bf52
include repoName pattern check + adjust doc comments
janthoXO Jul 11, 2025
f854ee5
adjust uri building to use segment builder
janthoXO Jul 11, 2025
c8fc3a0
adjust pattern match for repo type to be any character except newline
janthoXO Jul 11, 2025
b7e1a52
adjust pattern to repotype letters, numbers or dashes
janthoXO Jul 11, 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
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.core.security.Role;
import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools;
import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor;
Expand Down Expand Up @@ -171,6 +173,7 @@ public ResponseEntity<Result> getResult(@PathVariable Long participationId, @Pat
*/
@GetMapping("participations/{participationId}/results/{resultId}/details")
@EnforceAtLeastStudent
@AllowedTools(ToolTokenType.SCORPIO)
public ResponseEntity<List<Feedback>> getResultDetails(@PathVariable Long participationId, @PathVariable Long resultId) {
log.debug("REST request to get details of Result : {}", resultId);
Result result = resultRepository.findByIdWithEagerFeedbacksElseThrow(resultId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ public ResponseEntity<Void> reset(@PathVariable Long exerciseId) {
*/
@GetMapping("exercises/{exerciseId}/details")
@EnforceAtLeastStudent
@AllowedTools(ToolTokenType.SCORPIO)
public ResponseEntity<ExerciseDetailsDTO> getExerciseDetails(@PathVariable Long exerciseId) {
User user = userRepository.getUserWithGroupsAndAuthorities();
Exercise exercise = exerciseService.findOneWithDetailsForStudents(exerciseId, user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.net.URISyntaxException;
import java.util.Objects;

import org.springframework.web.util.UriComponentsBuilder;

/**
* Represents a Version Control System (VCS) repository URI with capabilities to manipulate and extract information from it.
* This class supports handling both local file references and remote repository URIs.
Expand Down Expand Up @@ -33,6 +35,22 @@ public VcsRepositoryUri(String uriSpecString) throws URISyntaxException {
this.uri = new URI(uriSpecString);
}

/**
* Initializes a new instance of the {@link VcsRepositoryUri} class from a repository name
* and builds an url to format <code>{server.url}/git/{project-key}/{repo-name}.git</code> with <code>repo-name</code> consisting of <code>{project-key}-{repo-type}</code>
*
* @param vcBaseUrl The base URL of the version control system
* @param repositoryName containing the project key at the beginning
*/
public VcsRepositoryUri(String vcBaseUrl, String repositoryName) throws URISyntaxException {
if (!repositoryName.matches("[a-zA-Z0-9]+-[a-zA-Z0-9-]+")) {
throw new IllegalArgumentException("Repository name must be in the format <project-key>-<repo-type>");
}

var projectKey = repositoryName.split("-")[0];
this.uri = UriComponentsBuilder.fromUriString(vcBaseUrl).pathSegment("git", projectKey.toUpperCase(), repositoryName + ".git").build().toUri();
}

/**
* Initializes a new instance of the {@link VcsRepositoryUri} class from a file reference, e.g. C:/Users/Admin/AppData/Local/Temp/studentOriginRepo1644180397872264950
* The file's URI is extracted and stored.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package de.tum.cit.aet.artemis.programming.dto;

import java.time.ZonedDateTime;
import java.util.Optional;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.assessment.domain.AssessmentType;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.exercise.domain.DifficultyLevel;
import de.tum.cit.aet.artemis.exercise.domain.ExerciseMode;
import de.tum.cit.aet.artemis.exercise.domain.ExerciseType;
import de.tum.cit.aet.artemis.exercise.domain.IncludedInOverallScore;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage;

/**
* DTO for the endpoint
* {@link de.tum.cit.aet.artemis.programming.web.ProgrammingExerciseParticipationResource#getStudentParticipationByRepoName(String)}
* constructing a participation DTO including exercise and course information
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record RepoNameProgrammingStudentParticipationDTO(long id, ZonedDateTime individualDueDate, String participantName, String participantIdentifier, String repositoryUri,
String buildPlanId, String branch, RepoNameProgrammingExerciseDTO exercise) {

/**
* Converts a ProgrammingExerciseStudentParticipation into a dto for the endpoint
* {@link de.tum.cit.aet.artemis.programming.web.ProgrammingExerciseParticipationResource#getStudentParticipationByRepoName(String)}.
*
* @param participation to convert
* @return the converted DTO
*/
public static RepoNameProgrammingStudentParticipationDTO of(ProgrammingExerciseStudentParticipation participation) {
return Optional
.ofNullable(participation).map(p -> new RepoNameProgrammingStudentParticipationDTO(p.getId(), p.getIndividualDueDate(), p.getParticipantName(),
p.getParticipantIdentifier(), p.getRepositoryUri(), p.getBuildPlanId(), p.getBranch(), RepoNameProgrammingExerciseDTO.of(p.getProgrammingExercise())))
.orElse(null);
}

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record RepoNameProgrammingExerciseDTO(long id, String problemStatement, String title, String shortName, ZonedDateTime releaseDate, ZonedDateTime startDate,
ZonedDateTime dueDate, ZonedDateTime assessmentDueDate, Double maxPoints, Double bonusPoints, AssessmentType assessmentType,
boolean allowComplaintsForAutomaticAssessments, boolean allowFeedbackRequests, DifficultyLevel difficulty, ExerciseMode mode,
IncludedInOverallScore includedInOverallScore, ExerciseType exerciseType, ZonedDateTime exampleSolutionPublicationDate, RepoNameCourseDTO course, String projectKey,
ProgrammingLanguage programmingLanguage, Boolean showTestNamesToStudents) {

/**
* Converts a ProgrammingExercise into a dto for the endpoint
* {@link de.tum.cit.aet.artemis.programming.web.ProgrammingExerciseParticipationResource#getStudentParticipationByRepoName(String)}.
*
* @param exercise to convert
* @return the converted DTO
*/
public static RepoNameProgrammingExerciseDTO of(ProgrammingExercise exercise) {
return Optional.ofNullable(exercise)
.map(e -> new RepoNameProgrammingExerciseDTO(e.getId(), e.getProblemStatement(), e.getTitle(), e.getShortName(), e.getReleaseDate(), e.getStartDate(),
e.getDueDate(), e.getAssessmentDueDate(), e.getMaxPoints(), e.getBonusPoints(), e.getAssessmentType(), e.getAllowComplaintsForAutomaticAssessments(),
e.getAllowFeedbackRequests(), e.getDifficulty(), e.getMode(), e.getIncludedInOverallScore(), e.getExerciseType(), e.getExampleSolutionPublicationDate(),
RepoNameCourseDTO.of(e.getCourseViaExerciseGroupOrCourseMember()), e.getProjectKey(), e.getProgrammingLanguage(), e.getShowTestNamesToStudents()))
.orElse(null);
}
}

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record RepoNameCourseDTO(long id, String title, String shortName) {

/**
* Converts a Course into a dto for the endpoint
* {@link de.tum.cit.aet.artemis.programming.web.ProgrammingExerciseParticipationResource#getStudentParticipationByRepoName(String)}.
*
* @param course to convert
* @return the converted DTO
*/
public static RepoNameCourseDTO of(Course course) {
return Optional.ofNullable(course).map(c -> new RepoNameCourseDTO(c.getId(), c.getTitle(), c.getShortName())).orElse(null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.io.IOException;
import java.net.URISyntaxException;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
Expand All @@ -14,9 +15,11 @@
import org.eclipse.jgit.api.errors.GitAPIException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
Expand All @@ -32,6 +35,8 @@
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException;
import de.tum.cit.aet.artemis.core.security.Role;
import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools;
import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor;
Expand All @@ -53,6 +58,7 @@
import de.tum.cit.aet.artemis.programming.domain.VcsAccessLog;
import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri;
import de.tum.cit.aet.artemis.programming.dto.CommitInfoDTO;
import de.tum.cit.aet.artemis.programming.dto.RepoNameProgrammingStudentParticipationDTO;
import de.tum.cit.aet.artemis.programming.dto.VcsAccessLogDTO;
import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository;
import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository;
Expand All @@ -74,6 +80,9 @@ public class ProgrammingExerciseParticipationResource {

private static final String ENTITY_NAME = "programmingExerciseParticipation";

@Value("${artemis.version-control.url}")
private String localVCBaseUrl;

private final ParticipationRepository participationRepository;

private final ProgrammingExerciseParticipationService programmingExerciseParticipationService;
Expand Down Expand Up @@ -180,6 +189,49 @@ public ResponseEntity<ProgrammingExerciseStudentParticipation> getParticipationW
return ResponseEntity.ok(participation);
}

/**
* Get the student participation by its repository identifier.
* The repository name is the last part of the repository URL.
* The repository URL is built as follows: <code>{server.url}/git/{project_key}/{repo-name}.git</code> with <code>{repo-name}</code> consisting of
* <code>{project-key}-{repo-type}</code>
*
* @param repoNameParam the URL repository identifier
* @return the ResponseEntity with status 200 (OK) and the participation DTO {@link de.tum.cit.aet.artemis.programming.dto.RepoNameProgrammingStudentParticipationDTO} in body,
* or with status 400 (Bad Request) if the repo name is not provided as request parameter,
* or with status 404 (Not Found) if the participation is not found,
* or with status 403 (Forbidden) if the user doesn't have access to the participation
*/
@GetMapping("programming-exercise-participations")
@EnforceAtLeastStudent
@AllowedTools(ToolTokenType.SCORPIO)
public ResponseEntity<RepoNameProgrammingStudentParticipationDTO> getStudentParticipationByRepoName(@RequestParam(required = true, name = "repoName") String repoNameParam) {
String repoUri;
if (!StringUtils.hasText(repoNameParam)) {
throw new BadRequestAlertException("Repository name must be provided", ENTITY_NAME, "repoNameRequired");
}

try {
repoUri = new VcsRepositoryUri(localVCBaseUrl, repoNameParam).toString();
}
catch (URISyntaxException e) {
throw new BadRequestAlertException("Invalid repository URL", ENTITY_NAME, "invalidRepositoryUrl");
}
catch (IllegalArgumentException e) {
throw new BadRequestAlertException("Invalid repository name", ENTITY_NAME, "invalidRepositoryName");
}

// find participation by url
var participation = programmingExerciseStudentParticipationRepository.findByRepositoryUriElseThrow(repoUri);

participationAuthCheckService.checkCanAccessParticipationElseThrow(participation);
// check if the exercise is released. This also checks if the user can see an exam exercise
if (!participation.getProgrammingExercise().isReleased()) {
throw new AccessForbiddenException("exercise", participation.getProgrammingExercise().getId());
}

return ResponseEntity.ok(RepoNameProgrammingStudentParticipationDTO.of(participation));
}

/**
* Get the latest result for a given programming exercise participation including its result.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
import static org.mockito.Mockito.doThrow;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;

import org.eclipse.jgit.api.errors.GitAPIException;
Expand Down Expand Up @@ -50,6 +53,7 @@
import de.tum.cit.aet.artemis.programming.domain.TemplateProgrammingExerciseParticipation;
import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri;
import de.tum.cit.aet.artemis.programming.dto.CommitInfoDTO;
import de.tum.cit.aet.artemis.programming.dto.RepoNameProgrammingStudentParticipationDTO;

class ProgrammingExerciseParticipationIntegrationTest extends AbstractProgrammingIntegrationIndependentTest {

Expand Down Expand Up @@ -645,6 +649,100 @@ void checkIfParticipationHasResult_withoutResult_returnsFalse() throws Exception
assertThat(response).isFalse();
}

@Test
@WithMockUser(username = TEST_PREFIX + "student1", roles = "USER")
void testGetProgrammingExerciseStudentParticipationByRepoName() throws Exception {
programmingExercise.setReleaseDate(ZonedDateTime.now());
programmingExercise = programmingExerciseRepository.save(programmingExercise);

var participation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student1");

var repoName = extractRepoName(participation.getRepositoryUri());
RepoNameProgrammingStudentParticipationDTO participationDTO = request.get("/api/programming/programming-exercise-participations?repoName=" + repoName, HttpStatus.OK,
RepoNameProgrammingStudentParticipationDTO.class);

assertThat(participationDTO.id()).isEqualTo(participation.getId());
assertThat(participationDTO.exercise().id()).isEqualTo(participation.getExercise().getId());
assertThat(participationDTO.exercise().course().id()).isEqualTo(participation.getExercise().getCourseViaExerciseGroupOrCourseMember().getId());
}

@Test
@WithMockUser(username = TEST_PREFIX + "student1", roles = "USER")
void testGetProgrammingExerciseStudentParticipationNoParam() throws Exception {
String body = request.get("/api/programming/programming-exercise-participations", HttpStatus.BAD_REQUEST, String.class);
}

@Test
@WithMockUser(username = TEST_PREFIX + "student1", roles = "USER")
void testGetProgrammingExerciseStudentParticipationByRepoNameNotFound() throws Exception {
var participation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student1");

// Generate a random URI that is not in the database
URI repoUrl;
Optional<ProgrammingExerciseStudentParticipation> foundParticipation;
do {
repoUrl = new URI(participation.getRepositoryUri());
repoUrl = new URI(repoUrl.getScheme(), repoUrl.getUserInfo(), repoUrl.getHost(), repoUrl.getPort(), "/" + UUID.randomUUID().toString(), repoUrl.getQuery(),
repoUrl.getFragment());
foundParticipation = programmingExerciseStudentParticipationRepository.findByRepositoryUri(repoUrl.toString());
}
while (foundParticipation.isPresent());

var repoName = extractRepoName(repoUrl.toString());
String body = request.get("/api/programming/programming-exercise-participations?repoName=" + repoName, HttpStatus.NOT_FOUND, String.class);
}

@Test
@WithMockUser(username = TEST_PREFIX + "student1", roles = "USER")
void testGetProgrammingExerciseStudentParticipationByInvalidRepoName() throws Exception {
var participation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student1");

// Generate a random URI that is not in the database
URI repoUrl;
Optional<ProgrammingExerciseStudentParticipation> foundParticipation;
do {
repoUrl = new URI(participation.getRepositoryUri());

// test a repoName which does not match the expected pattern of <project_key>-<repo-type>
// generate random string without a dash
String invalidRepoName = UUID.randomUUID().toString().replace("-", "");
repoUrl = new URI(repoUrl.getScheme(), repoUrl.getUserInfo(), repoUrl.getHost(), repoUrl.getPort(), "/" + invalidRepoName, repoUrl.getQuery(), repoUrl.getFragment());
foundParticipation = programmingExerciseStudentParticipationRepository.findByRepositoryUri(repoUrl.toString());
}
while (foundParticipation.isPresent());

var repoName = extractRepoName(repoUrl.toString());
String body = request.get("/api/programming/programming-exercise-participations?repoName=" + repoName, HttpStatus.BAD_REQUEST, String.class);
}

@Test
@WithMockUser(username = TEST_PREFIX + "student1", roles = "USER")
void testGetProgrammingExerciseStudentParticipationByRepoNameNotVisible() throws Exception {
var participation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student2");

var repoName = extractRepoName(participation.getRepositoryUri());
String body = request.get("/api/programming/programming-exercise-participations?repoName=" + repoName, HttpStatus.FORBIDDEN, String.class);
}

@Test
@WithMockUser(username = TEST_PREFIX + "student1", roles = "USER")
void testGetProgrammingExerciseStudentParticipationByRepoNameExam() throws Exception {
var programmingExercise = programmingExerciseUtilService.addCourseExamExerciseGroupWithProgrammingExerciseAndExamDates(ZonedDateTime.now().plusHours(1),
ZonedDateTime.now().plusHours(2), ZonedDateTime.now().plusHours(3), ZonedDateTime.now().plusHours(4), TEST_PREFIX + "student1", 1000);
programmingExercise.setReleaseDate(ZonedDateTime.now());
programmingExercise = programmingExerciseRepository.save(programmingExercise);

var participation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student1");

var repoName = extractRepoName(participation.getRepositoryUri());
String body = request.get("/api/programming/programming-exercise-participations?repoName=" + repoName, HttpStatus.FORBIDDEN, String.class);
}

String extractRepoName(String repoUrl) {
// <server.url>/git/<project_key>/<repo-name>.git
return repoUrl.substring(repoUrl.lastIndexOf("/") + 1, repoUrl.length() - 4);
}

@Test
@WithMockUser(username = TEST_PREFIX + "student1", roles = "USER")
void checkResetRepository_noAccess_forbidden() throws Exception {
Expand Down
Loading