Skip to content

Commit

Permalink
Exam mode: Visualize student submissions in exam timeline (#6882)
Browse files Browse the repository at this point in the history
  • Loading branch information
tobias-lippert authored Oct 21, 2023
1 parent ec8178e commit 62c9608
Show file tree
Hide file tree
Showing 98 changed files with 3,357 additions and 164 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const esModules = [
'ngx-infinite-scroll',
'internmap',
'@swimlane/ngx-graph',
'ngx-slider-v2'
].join('|');

const {
Expand Down
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@angular/platform-browser-dynamic": "16.2.7",
"@angular/router": "16.2.7",
"@angular/service-worker": "16.2.7",
"ngx-slider-v2": "16.0.2",
"@ctrl/ngx-emoji-mart": "9.2.0",
"@danielmoncada/angular-datetime-picker": "16.0.1",
"@fingerprintjs/fingerprintjs": "4.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ default ProgrammingExercise findOneByProjectKeyOrThrow(String projectKey, boolea
""")
Optional<ProgrammingExercise> findByParticipationId(@Param("participationId") Long participationId);

default ProgrammingExercise findByParticipationIdOrElseThrow(long participationId) throws EntityNotFoundException {
return findByParticipationId(participationId)
.orElseThrow(() -> new EntityNotFoundException("Programming exercise for participation with id " + participationId + " does not exist"));
}

@Query("""
SELECT pe
FROM ProgrammingExercise pe
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,4 +440,8 @@ default Submission findOneWithEagerResultAndFeedback(long submissionId) {
default Submission findByIdWithResultsElseThrow(long submissionId) {
return findWithEagerResultsAndAssessorById(submissionId).orElseThrow(() -> new EntityNotFoundException("Submission", +submissionId));
}

default Submission findByIdElseThrow(long submissionId) {
return findById(submissionId).orElseThrow(() -> new EntityNotFoundException("Submission", submissionId));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.tum.in.www1.artemis.repository;

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
Expand Down Expand Up @@ -28,4 +29,6 @@ SELECT max(id)
""")
Optional<SubmissionVersion> findLatestVersion(@Param("submissionId") long submissionId);

List<SubmissionVersion> findSubmissionVersionBySubmissionIdOrderByCreatedDateAsc(long submissionId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import de.tum.in.www1.artemis.service.ProfileService;
import de.tum.in.www1.artemis.service.ZipFileService;
import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUrl;
import de.tum.in.www1.artemis.web.rest.dto.CommitInfoDTO;
import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;

@Service
Expand Down Expand Up @@ -374,6 +375,40 @@ public Repository getOrCheckoutRepository(VcsRepositoryUrl repoUrl, String targe
return getOrCheckoutRepository(repoUrl, localPath, pullOnGet);
}

/**
* Checkout at the given repository at the given commit hash
*
* @param repository the repository to check out the commit in
* @param commitHash the hash of the commit to check out
* @return the repository checked out at the given commit
*/
public Repository checkoutRepositoryAtCommit(Repository repository, String commitHash) {
try (Git git = new Git(repository)) {
git.checkout().setName(commitHash).call();
}
catch (GitAPIException e) {
throw new GitException("Could not checkout commit " + commitHash + " in repository located at " + repository.getLocalPath(), e);
}
return repository;
}

/**
* Get the local repository for a given remote repository URL.
* <p>
* If the local repo does not exist yet, it will be checked out.
* After retrieving the repository, the commit for the given hash will be checked out.
*
* @param vcsRepositoryUrl the url of the remote repository
* @param commitHash the hash of the commit to checkout
* @param pullOnGet pull from the remote on the checked out repository, if it does not need to be cloned
* @return the repository if it could be checked out
* @throws GitAPIException if the repository could not be checked out
*/
public Repository checkoutRepositoryAtCommit(VcsRepositoryUrl vcsRepositoryUrl, String commitHash, boolean pullOnGet) throws GitAPIException {
var repository = getOrCheckoutRepository(vcsRepositoryUrl, pullOnGet);
return checkoutRepositoryAtCommit(repository, commitHash);
}

/**
* Get the local repository for a given remote repository URL. If the local repo does not exist yet, it will be checked out.
*
Expand Down Expand Up @@ -872,6 +907,18 @@ public void resetToOriginHead(Repository repo) {
}
}

/**
* Switch back to the HEAD commit of the default branch.
*
* @param repository the repository for which we want to switch to the HEAD commit of the default branch
* @throws GitAPIException if this operation fails
*/
public void switchBackToDefaultBranchHead(Repository repository) throws GitAPIException {
try (Git git = new Git(repository)) {
git.checkout().setName(defaultBranch).call();
}
}

/**
* Get last commit hash from HEAD
*
Expand Down Expand Up @@ -1357,4 +1404,28 @@ private LsRemoteCommand lsRemoteCommand() {
public <C extends GitCommand<?>> C authenticate(TransportCommand<C, ?> command) {
return command.setTransportConfigCallback(sshCallback);
}

/**
* Checkout a repository and get the git log for a given repository url.
*
* @param vcsRepositoryUrl the repository url for which the git log should be retrieved
* @return a list of commit info DTOs containing author, timestamp, commit message, and hash
* @throws GitAPIException if an error occurs while retrieving the git log
*/
public List<CommitInfoDTO> getCommitInfos(VcsRepositoryUrl vcsRepositoryUrl) throws GitAPIException {
List<CommitInfoDTO> commitInfos = new ArrayList<>();

try (var repo = getOrCheckoutRepository(vcsRepositoryUrl, true); var git = new Git(repo)) {
var commits = git.log().call();
commits.forEach(commit -> {
var commitInfo = CommitInfoDTO.of(commit);
commitInfos.add(commitInfo);
});
}
return commitInfos;
}

public void clearCachedRepositories() {
cachedRepositories.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,29 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.validation.constraints.NotNull;

import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;

import de.tum.in.www1.artemis.domain.DomainObject;
import de.tum.in.www1.artemis.domain.ProgrammingExercise;
import de.tum.in.www1.artemis.domain.VcsRepositoryUrl;
import de.tum.in.www1.artemis.domain.*;
import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseGitDiffEntry;
import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseGitDiffReport;
import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation;
import de.tum.in.www1.artemis.domain.participation.SolutionProgrammingExerciseParticipation;
import de.tum.in.www1.artemis.domain.participation.TemplateProgrammingExerciseParticipation;
import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository;
import de.tum.in.www1.artemis.repository.ProgrammingSubmissionRepository;
import de.tum.in.www1.artemis.repository.SolutionProgrammingExerciseParticipationRepository;
import de.tum.in.www1.artemis.repository.TemplateProgrammingExerciseParticipationRepository;
import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseGitDiffReportRepository;
import de.tum.in.www1.artemis.service.FileService;
import de.tum.in.www1.artemis.service.connectors.GitService;
import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException;

Expand All @@ -53,18 +56,21 @@ public class ProgrammingExerciseGitDiffReportService {

private final SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository;

private final FileService fileService;

private final Pattern gitDiffLinePattern = Pattern.compile("@@ -(?<previousLine>\\d+)(,(?<previousLineCount>\\d+))? \\+(?<newLine>\\d+)(,(?<newLineCount>\\d+))? @@");

public ProgrammingExerciseGitDiffReportService(GitService gitService, ProgrammingExerciseGitDiffReportRepository programmingExerciseGitDiffReportRepository,
ProgrammingSubmissionRepository programmingSubmissionRepository, ProgrammingExerciseRepository programmingExerciseRepository,
TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository,
SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository) {
SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, FileService fileService) {
this.gitService = gitService;
this.programmingExerciseGitDiffReportRepository = programmingExerciseGitDiffReportRepository;
this.programmingSubmissionRepository = programmingSubmissionRepository;
this.programmingExerciseRepository = programmingExerciseRepository;
this.templateProgrammingExerciseParticipationRepository = templateProgrammingExerciseParticipationRepository;
this.solutionProgrammingExerciseParticipationRepository = solutionProgrammingExerciseParticipationRepository;
this.fileService = fileService;
}

/**
Expand Down Expand Up @@ -160,6 +166,29 @@ public ProgrammingExerciseGitDiffReport getOrCreateReportOfExercise(ProgrammingE
}
}

/**
* Creates a new ProgrammingExerciseGitDiffReport for a submission with the template repository.
*
* @param exercise The exercise for which the report should be created
* @param submission The submission for which the report should be created
* @return The report with the changes between the submission and the template
* @throws GitAPIException If an error occurs while accessing the git repository
* @throws IOException If an error occurs while accessing the file system
*/
public ProgrammingExerciseGitDiffReport createReportForSubmissionWithTemplate(ProgrammingExercise exercise, ProgrammingSubmission submission)
throws GitAPIException, IOException {
var templateParticipation = templateProgrammingExerciseParticipationRepository.findByProgrammingExerciseId(exercise.getId()).orElseThrow();
Repository templateRepo = prepareTemplateRepository(templateParticipation);

var repo1 = gitService.checkoutRepositoryAtCommit(((ProgrammingExerciseParticipation) submission.getParticipation()).getVcsRepositoryUrl(), submission.getCommitHash(),
false);
var oldTreeParser = new FileTreeIterator(templateRepo);
var newTreeParser = new FileTreeIterator(repo1);
var report = createReport(templateRepo, oldTreeParser, newTreeParser);
gitService.switchBackToDefaultBranchHead(repo1);
return report;
}

/**
* Calculates git diff between two repositories and returns the cumulative number of diff lines.
*
Expand Down Expand Up @@ -198,18 +227,88 @@ public int calculateNumberOfDiffLinesBetweenRepos(VcsRepositoryUrl urlRepoA, Pat
*/
private ProgrammingExerciseGitDiffReport generateReport(TemplateProgrammingExerciseParticipation templateParticipation,
SolutionProgrammingExerciseParticipation solutionParticipation) throws GitAPIException, IOException {
var templateRepo = gitService.getOrCheckoutRepository(templateParticipation.getVcsRepositoryUrl(), true);
Repository templateRepo = prepareTemplateRepository(templateParticipation);
var solutionRepo = gitService.getOrCheckoutRepository(solutionParticipation.getVcsRepositoryUrl(), true);

gitService.resetToOriginHead(templateRepo);
gitService.pullIgnoreConflicts(templateRepo);
gitService.resetToOriginHead(solutionRepo);
gitService.pullIgnoreConflicts(solutionRepo);

var oldTreeParser = new FileTreeIterator(templateRepo);
var newTreeParser = new FileTreeIterator(solutionRepo);

try (ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream(); Git git = Git.wrap(templateRepo)) {
return createReport(templateRepo, oldTreeParser, newTreeParser);
}

/**
* Prepares the template repository for the git diff calculation by checking it out and resetting it to the origin head.
*
* @param templateParticipation The participation for the template
* @return The checked out template repository
* @throws GitAPIException If an error occurs while accessing the git repository
*/
private Repository prepareTemplateRepository(TemplateProgrammingExerciseParticipation templateParticipation) throws GitAPIException {
var templateRepo = gitService.getOrCheckoutRepository(templateParticipation.getVcsRepositoryUrl(), true);
gitService.resetToOriginHead(templateRepo);
gitService.pullIgnoreConflicts(templateRepo);
return templateRepo;
}

/**
* Creates a new ProgrammingExerciseGitDiffReport containing the git-diff for two submissions.
*
* @param submission1 The first submission (older)
* @param submission2 The second submission (newer)
* @return The report with the changes between the two submissions
* @throws GitAPIException If an error occurs while accessing the git repository
* @throws IOException If an error occurs while accessing the file system
*/
public ProgrammingExerciseGitDiffReport generateReportForSubmissions(ProgrammingSubmission submission1, ProgrammingSubmission submission2) throws GitAPIException, IOException {
var repositoryUrl = ((ProgrammingExerciseParticipation) submission1.getParticipation()).getVcsRepositoryUrl();
var repo1 = gitService.getOrCheckoutRepository(repositoryUrl, true);
var repo1Path = repo1.getLocalPath();
var repo2Path = fileService.getTemporaryUniqueSubfolderPath(repo1Path.getParent(), 5);
FileSystemUtils.copyRecursively(repo1Path, repo2Path);
repo1 = gitService.checkoutRepositoryAtCommit(repo1, submission1.getCommitHash());
var repo2 = gitService.getExistingCheckedOutRepositoryByLocalPath(repo2Path, repositoryUrl);
repo2 = gitService.checkoutRepositoryAtCommit(repo2, submission2.getCommitHash());
return parseFilesAndCreateReport(repo1, repo2);
}

/**
* Parses the files of the given repositories and creates a new ProgrammingExerciseGitDiffReport containing the git-diff.
*
* @param repo1 The first repository
* @param repo2 The second repository
* @return The report with the changes between the two repositories at their checked out state
* @throws IOException If an error occurs while accessing the file system
* @throws GitAPIException If an error occurs while accessing the git repository
*/
@NotNull
private ProgrammingExerciseGitDiffReport parseFilesAndCreateReport(Repository repo1, Repository repo2) throws IOException, GitAPIException {
var oldTreeParser = new FileTreeIterator(repo1);
var newTreeParser = new FileTreeIterator(repo2);

var report = createReport(repo1, oldTreeParser, newTreeParser);
gitService.switchBackToDefaultBranchHead(repo1);
gitService.switchBackToDefaultBranchHead(repo2);
return report;
}

/**
* Creates a new ProgrammingExerciseGitDiffReport containing the git-diff.
* <p>
* It parses all files of the repositories in their directories on the file system and creates a report containing the changes.
* Both repositories have to be checked out at the commit that should be compared and be in different directories
*
* @param repo1 The first repository
* @param oldTreeParser The tree parser for the first repository
* @param newTreeParser The tree parser for the second repository
* @return The report with the changes between the two repositories at their checked out state
* @throws IOException If an error occurs while accessing the file system
* @throws GitAPIException If an error occurs while accessing the git repository
*/
@NotNull
private ProgrammingExerciseGitDiffReport createReport(Repository repo1, FileTreeIterator oldTreeParser, FileTreeIterator newTreeParser) throws IOException, GitAPIException {
try (ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream(); Git git = Git.wrap(repo1)) {
git.diff().setOldTree(oldTreeParser).setNewTree(newTreeParser).setOutputStream(diffOutputStream).call();
var diff = diffOutputStream.toString();
var programmingExerciseGitDiffEntries = extractDiffEntries(diff, false);
Expand Down
Loading

0 comments on commit 62c9608

Please sign in to comment.