Skip to content

Commit

Permalink
Bugfix/programming exercise/fix broken endpoints (#838)
Browse files Browse the repository at this point in the history
  • Loading branch information
thilo-behnke authored and Stephan Krusche committed Sep 23, 2019
1 parent b417c41 commit a714635
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,7 @@ public void delete(Exercise exercise, boolean deleteStudentReposBuildPlans, bool
participationService.deleteAllByExerciseId(exercise.getId(), deleteStudentReposBuildPlans, deleteStudentReposBuildPlans);
// Programming exercises have some special stuff that needs to be cleaned up (solution/template participation, build plans, etc.).
if (exercise instanceof ProgrammingExercise) {
ProgrammingExercise programmingExercise = (ProgrammingExercise) exercise;
programmingExerciseService.get().delete(programmingExercise, deleteBaseReposBuildPlans);
programmingExerciseService.get().delete(exercise.getId(), deleteBaseReposBuildPlans);
}
else {
exerciseRepository.delete(exercise);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,29 @@ public void squashAllCommitsOfRepositoryIntoOne(URL repoUrl) throws InterruptedE
gitService.squashAllCommitsIntoInitialCommit(exerciseRepository);
}

/**
* Updates the problem statement of the given programming exercise.
*
* @param programmingExerciseId ProgrammingExercise Id.
* @param problemStatement markdown of the problem statement.
* @return the updated ProgrammingExercise object.
* @throws EntityNotFoundException if there is no ProgrammingExercise for the given id.
* @throws IllegalAccessException if the user does not have permissions to access the ProgrammingExercise.
*/
public ProgrammingExercise updateProblemStatement(Long programmingExerciseId, String problemStatement) throws EntityNotFoundException, IllegalAccessException {
Optional<ProgrammingExercise> programmingExerciseOpt = programmingExerciseRepository.findById(programmingExerciseId);
if (programmingExerciseOpt.isEmpty()) {
throw new EntityNotFoundException("Programming exercise not found with id: " + programmingExerciseId);
}
ProgrammingExercise programmingExercise = programmingExerciseOpt.get();
User user = userService.getUserWithGroupsAndAuthorities();
if (!authCheckService.isAtLeastInstructorForExercise(programmingExercise, user)) {
throw new IllegalAccessException("User with login " + user.getLogin() + " is not authorized to access programming exercise with id: " + programmingExerciseId);
}
programmingExercise.setProblemStatement(problemStatement);
return programmingExercise;
}

/**
* This method calls the StructureOracleGenerator, generates the string out of the JSON representation of the structure oracle of the programming exercise and returns true if
* the file was updated or generated, false otherwise. This can happen if the contents of the file have not changed.
Expand Down Expand Up @@ -702,11 +725,13 @@ public boolean generateStructureOracleFile(URL solutionRepoURL, URL exerciseRepo
/**
* Delete a programming exercise, including its template and solution participations.
*
* @param programmingExercise to delete.
* @param programmingExerciseId id of the programming exercise to delete.
* @param deleteBaseReposBuildPlans if true will also delete build plans and projects.
*/
@Transactional
public void delete(ProgrammingExercise programmingExercise, boolean deleteBaseReposBuildPlans) {
public void delete(Long programmingExerciseId, boolean deleteBaseReposBuildPlans) {
// TODO: This method does not accept a programming exercise to solve issues with nested Transactions.
// It would be good to refactor the delete calls and move the validity checks down from the resources to the service methods (e.g. EntityNotFound).
ProgrammingExercise programmingExercise = programmingExerciseRepository.findById(programmingExerciseId).get();
if (deleteBaseReposBuildPlans) {
if (programmingExercise.getTemplateBuildPlanId() != null) {
continuousIntegrationService.get().deleteBuildPlan(programmingExercise.getTemplateBuildPlanId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import de.tum.in.www1.artemis.service.connectors.ContinuousIntegrationService;
import de.tum.in.www1.artemis.service.connectors.VersionControlService;
import de.tum.in.www1.artemis.service.scheduled.ProgrammingExerciseScheduleService;
import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;
import de.tum.in.www1.artemis.web.rest.util.HeaderUtil;
import io.github.jhipster.web.util.ResponseUtil;

Expand Down Expand Up @@ -341,39 +342,28 @@ public ResponseEntity<ProgrammingExercise> updateProgrammingExercise(@RequestBod
*
* @param problemStatementUpdate the programmingExercise to update with the new problemStatement
* @param notificationText to notify the student group about the updated problemStatement on the programming exercise
* @return the ResponseEntity with status 200 (OK) and with body the updated problemStatement, or with status 400 (Bad Request) if the programmingExercise is not valid, or with
* status 500 (Internal Server Error) if the programmingExercise couldn't be updated.
* @throws URISyntaxException if the Location URI syntax is incorrect
* @return the ResponseEntity with status 200 (OK) and with body the updated problemStatement, with status 404 if the programmingExercise could not be found, or with 403 if the user does not have permissions to access the programming exercise.
*/
@PatchMapping("/programming-exercises-problem")
@PreAuthorize("hasAnyRole('TA', 'INSTRUCTOR', 'ADMIN')")
public ResponseEntity<ProgrammingExercise> updateProblemStatement(@RequestBody ProblemStatementUpdate problemStatementUpdate,
@RequestParam(value = "notificationText", required = false) String notificationText) throws URISyntaxException {
@RequestParam(value = "notificationText", required = false) String notificationText) {
log.debug("REST request to update ProgrammingExercise with new problem statement: {}", problemStatementUpdate);
// fetch course from database to make sure client didn't change groups
ProgrammingExercise programmingExercise = (ProgrammingExercise) exerciseService.findOne(problemStatementUpdate.getExerciseId());
Course course = courseService.findOne(programmingExercise.getCourse().getId());
if (course == null) {
return ResponseEntity.badRequest()
.headers(HeaderUtil.createAlert(applicationName, "courseNotFound", "The course belonging to this programming exercise does not exist")).body(null);
ProgrammingExercise updatedProgrammingExercise;
try {
updatedProgrammingExercise = programmingExerciseService.updateProblemStatement(problemStatementUpdate.getExerciseId(), problemStatementUpdate.getProblemStatement());
}
User user = userService.getUserWithGroupsAndAuthorities();
if (!authCheckService.isAtLeastTeachingAssistantInCourse(course, user)) {
catch (IllegalAccessException ex) {
return forbidden();
}

ResponseEntity<ProgrammingExercise> errorResponse = checkProgrammingExerciseForError(programmingExercise);
if (errorResponse != null) {
return errorResponse;
catch (EntityNotFoundException ex) {
return notFound();
}

programmingExercise.setProblemStatement(problemStatementUpdate.getProblemStatement());

ProgrammingExercise result = programmingExerciseRepository.save(programmingExercise);
if (notificationText != null) {
groupNotificationService.notifyStudentGroupAboutExerciseUpdate(result, notificationText);
groupNotificationService.notifyStudentGroupAboutExerciseUpdate(updatedProgrammingExercise, notificationText);
}
return ResponseEntity.ok().headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, programmingExercise.getTitle())).body(result);
return ResponseEntity.ok().headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, updatedProgrammingExercise.getTitle()))
.body(updatedProgrammingExercise);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ export class ProgrammingExerciseUpdateComponent implements OnInit {
*/
set selectedProgrammingLanguage(language: ProgrammingLanguage) {
this.selectedProgrammingLanguageValue = language;
this.loadProgrammingLanguageTemplate(language);
// Don't override the problem statement with the template in edit mode.
if (this.programmingExercise.id === undefined) {
this.loadProgrammingLanguageTemplate(language);
}
}

get selectedProgrammingLanguage() {
Expand Down
122 changes: 122 additions & 0 deletions src/test/java/de/tum/in/www1/artemis/ProgrammingExerciseTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package de.tum.in.www1.artemis;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import de.tum.in.www1.artemis.domain.ProgrammingExercise;
import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository;
import de.tum.in.www1.artemis.security.SecurityUtils;
import de.tum.in.www1.artemis.service.connectors.BambooService;
import de.tum.in.www1.artemis.service.connectors.BitbucketService;
import de.tum.in.www1.artemis.util.DatabaseUtilService;
import de.tum.in.www1.artemis.util.RequestUtilService;
import de.tum.in.www1.artemis.web.rest.ProblemStatementUpdate;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureTestDatabase
@ActiveProfiles("artemis, bamboo")
class ProgrammingExerciseTest {

@Autowired
DatabaseUtilService database;

@Autowired
RequestUtilService request;

@Autowired
ProgrammingExerciseRepository programmingExerciseRepository;

@MockBean
BambooService continuousIntegrationService;

@MockBean
BitbucketService versionControlService;

Long programmingExerciseId;

@BeforeEach
void init() {
database.addUsers(2, 2, 2);
database.addCourseWithOneProgrammingExercise();

programmingExerciseId = programmingExerciseRepository.findAll().get(0).getId();
}

@AfterEach
void tearDown() {
database.resetDatabase();
}

void updateProgrammingExercise(ProgrammingExercise programmingExercise, String newProblem, String newTitle) throws Exception {
programmingExercise.setProblemStatement(newProblem);
programmingExercise.setTitle(newTitle);
when(continuousIntegrationService.buildPlanIdIsValid(programmingExercise.getTemplateBuildPlanId())).thenReturn(true);
when(versionControlService.repositoryUrlIsValid(programmingExercise.getTemplateRepositoryUrlAsUrl())).thenReturn(true);
when(continuousIntegrationService.buildPlanIdIsValid(programmingExercise.getSolutionBuildPlanId())).thenReturn(true);
when(versionControlService.repositoryUrlIsValid(programmingExercise.getSolutionRepositoryUrlAsUrl())).thenReturn(true);

ProgrammingExercise updatedProgrammingExercise = request.putWithResponseBody("/api/programming-exercises", programmingExercise, ProgrammingExercise.class, HttpStatus.OK);

// The result from the put response should be updated with the new data.
assertThat(updatedProgrammingExercise.getProblemStatement()).isEqualTo(newProblem);
assertThat(updatedProgrammingExercise.getTitle()).isEqualTo(newTitle);

SecurityUtils.setAuthorizationObject();
// There should still be only 1 programming exercise.
assertThat(programmingExerciseRepository.count()).isEqualTo(1);
// The programming exercise in the db should also be updated.
ProgrammingExercise fromDb = programmingExerciseRepository.findById(programmingExercise.getId()).get();
assertThat(fromDb.getProblemStatement()).isEqualTo(newProblem);
assertThat(fromDb.getTitle()).isEqualTo(newTitle);
}

@Test
@WithMockUser(value = "instructor1", roles = "INSTRUCTOR")
void updateProgrammingExerciseOnce() throws Exception {
ProgrammingExercise programmingExercise = programmingExerciseRepository.findById(programmingExerciseId).get();
updateProgrammingExercise(programmingExercise, "new problem 1", "new title 1");
}

@Test
@WithMockUser(value = "instructor1", roles = "INSTRUCTOR")
void updateProgrammingExerciseTwice() throws Exception {
ProgrammingExercise programmingExercise = programmingExerciseRepository.findById(programmingExerciseId).get();
updateProgrammingExercise(programmingExercise, "new problem 1", "new title 1");
updateProgrammingExercise(programmingExercise, "new problem 2", "new title 2");
}

@Test
@WithMockUser(value = "instructor1", roles = "INSTRUCTOR")
void updateProblemStatement() throws Exception {
String newProblem = "a new problem statement";
ProgrammingExercise programmingExercise = programmingExerciseRepository.findById(programmingExerciseId).get();
ProblemStatementUpdate problemStatementUpdate = new ProblemStatementUpdate();
problemStatementUpdate.setExerciseId(programmingExerciseId);
problemStatementUpdate.setProblemStatement(newProblem);
ProgrammingExercise updatedProgrammingExercise = request.patchWithResponseBody("/api/programming-exercises-problem", problemStatementUpdate, ProgrammingExercise.class,
HttpStatus.OK);

assertThat(updatedProgrammingExercise.getProblemStatement()).isEqualTo(newProblem);

SecurityUtils.setAuthorizationObject();
ProgrammingExercise fromDb = programmingExerciseRepository.findById(programmingExerciseId).get();
assertThat(fromDb.getProblemStatement()).isEqualTo(newProblem);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -313,8 +314,10 @@ void shouldCreateSubmissionForManualBuildRun(IntegrationTestParticipationType pa
@EnumSource(IntegrationTestParticipationType.class)
@WithMockUser(username = "instructor1", roles = "INSTRUCTOR")
void shouldTriggerManualBuildRunForLastCommit(IntegrationTestParticipationType participationType) throws Exception {
Long participationId = getParticipationIdByType(participationType, 0);
ObjectId objectId = ObjectId.fromString("9b3a9bd71a0d80e5bbc42204c319ed3d1d4f0d6d");
when(gitServiceMock.getLastCommitHash(null)).thenReturn(objectId);
URL repositoryUrl = ((ProgrammingExerciseParticipation) participationRepository.findById(participationId).get()).getRepositoryUrlAsUrl();
when(gitServiceMock.getLastCommitHash(repositoryUrl)).thenReturn(objectId);
triggerBuild(participationType, 0, HttpStatus.OK);

// Now a submission for the manual build should exist.
Expand All @@ -325,7 +328,6 @@ void shouldTriggerManualBuildRunForLastCommit(IntegrationTestParticipationType p
assertThat(submission.getType()).isEqualTo(SubmissionType.MANUAL);
assertThat(submission.isSubmitted()).isTrue();

Long participationId = getParticipationIdByType(participationType, 0);
SecurityUtils.setAuthorizationObject();
Participation participation = participationRepository.getOneWithEagerSubmissions(participationId);

Expand All @@ -349,8 +351,10 @@ void shouldTriggerManualBuildRunForLastCommit(IntegrationTestParticipationType p
@EnumSource(IntegrationTestParticipationType.class)
@WithMockUser(username = "instructor1", roles = "INSTRUCTOR")
void shouldTriggerInstructorBuildRunForLastCommit(IntegrationTestParticipationType participationType) throws Exception {
Long participationId = getParticipationIdByType(participationType, 0);
URL repositoryUrl = ((ProgrammingExerciseParticipation) participationRepository.findById(participationId).get()).getRepositoryUrlAsUrl();
ObjectId objectId = ObjectId.fromString("9b3a9bd71a0d80e5bbc42204c319ed3d1d4f0d6d");
when(gitServiceMock.getLastCommitHash(null)).thenReturn(objectId);
when(gitServiceMock.getLastCommitHash(repositoryUrl)).thenReturn(objectId);
triggerInstructorBuild(participationType, 0, HttpStatus.OK);

// Now a submission for the manual build should exist.
Expand All @@ -361,7 +365,6 @@ void shouldTriggerInstructorBuildRunForLastCommit(IntegrationTestParticipationTy
assertThat(submission.getType()).isEqualTo(SubmissionType.INSTRUCTOR);
assertThat(submission.isSubmitted()).isTrue();

Long participationId = getParticipationIdByType(participationType, 0);
SecurityUtils.setAuthorizationObject();
Participation participation = participationRepository.getOneWithEagerSubmissions(participationId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ public ProgrammingExercise addTemplateParticipationForProgrammingExercise(Progra
participation.setProgrammingExercise(exercise);
participation.setBuildPlanId("TEST201904BPROGRAMMINGEXERCISE6-BASE");
participation.setInitializationState(InitializationState.INITIALIZED);
participation.setRepositoryUrl("http://url/scm/TEST234454TEST234565/template.git");
templateProgrammingExerciseParticipationRepo.save(participation);
exercise.setTemplateParticipation(participation);
return programmingExerciseRepository.save(exercise);
Expand All @@ -216,6 +217,7 @@ public ProgrammingExercise addSolutionParticipationForProgrammingExercise(Progra
participation.setProgrammingExercise(exercise);
participation.setBuildPlanId("TEST201904BPROGRAMMINGEXERCISE6-SOLUTION");
participation.setInitializationState(InitializationState.INITIALIZED);
participation.setRepositoryUrl("http://url/scm/TEST234454TEST234565/solution.git");
solutionProgrammingExerciseParticipationRepo.save(participation);
exercise.setSolutionParticipation(participation);
return programmingExerciseRepository.save(exercise);
Expand Down Expand Up @@ -322,7 +324,8 @@ public void addCourseWithDifferentModelingExercises() {

public Course addCourseWithOneProgrammingExercise() {
Course course = ModelFactory.generateCourse(null, pastTimestamp, futureFutureTimestamp, new HashSet<>(), "tumuser", "tutor", "instructor");
ProgrammingExercise programmingExercise = (ProgrammingExercise) new ProgrammingExercise().programmingLanguage(ProgrammingLanguage.JAVA).course(course);
ProgrammingExercise programmingExercise = (ProgrammingExercise) new ProgrammingExercise().programmingLanguage(ProgrammingLanguage.JAVA).course(course)
.title("programming exercise");
courseRepo.save(course);
programmingExerciseRepository.save(programmingExercise);
course.addExercises(programmingExercise);
Expand Down
Loading

0 comments on commit a714635

Please sign in to comment.