Skip to content

Commit

Permalink
Feature/build plan cleanup (#33)
Browse files Browse the repository at this point in the history
* REST resource and service method for build plan cleanup
* added button for build plan cleanup
* started work on the build plan delete modal
* solved error with no default controllerAs value; created translation files for instructor dashboard
* created call to REST resource to delete build plans
* changed how id is passed to GET request to get exercise; minor cleanup
* added translations for inactive participation state
* create REST request and service method to resume participation
* changed order of ParticipationState elements
* unified service calls to find initialized and inactive participations into one
* added button to resume participation
* added resource method for resuming participation
* removed explicit return of internal server error in ParticipationResource
* added alert
* removed plain javascript alert on build plans cleanup
* removed plain javascript alert
* request for changes addressed
* fixed error with logging message not in proper place
  • Loading branch information
VitaNuova authored and krusche committed Oct 8, 2017
1 parent ec9e54a commit 43d1f12
Show file tree
Hide file tree
Showing 19 changed files with 271 additions and 21 deletions.
Expand Up @@ -4,7 +4,7 @@
* The ParticipationState enumeration.
*/
public enum ParticipationState {
UNINITIALIZED(0), REPO_COPIED(1), REPO_CONFIGURED(2), BUILD_PLAN_COPIED(3), BUILD_PLAN_CONFIGURED(4), INITIALIZED(5);
UNINITIALIZED(0), REPO_COPIED(1), REPO_CONFIGURED(2), INACTIVE(3), BUILD_PLAN_COPIED(4), BUILD_PLAN_CONFIGURED(5), INITIALIZED(6);

private Integer stateNumber;

Expand Down
@@ -1,9 +1,7 @@
package de.tum.in.www1.exerciseapp.service;

import de.tum.in.www1.exerciseapp.domain.Authority;
import de.tum.in.www1.exerciseapp.domain.Exercise;
import de.tum.in.www1.exerciseapp.domain.Participation;
import de.tum.in.www1.exerciseapp.domain.User;
import de.tum.in.www1.exerciseapp.domain.*;
import de.tum.in.www1.exerciseapp.domain.enumeration.ParticipationState;
import de.tum.in.www1.exerciseapp.repository.ExerciseRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -30,11 +28,13 @@ public class ExerciseService {
private final ExerciseRepository exerciseRepository;
private final UserService userService;
private final ParticipationService participationService;
private final Optional<ContinuousIntegrationService> continuousIntegrationService;

public ExerciseService(ExerciseRepository exerciseRepository, UserService userService, ParticipationService participationService) {
public ExerciseService(ExerciseRepository exerciseRepository, UserService userService, ParticipationService participationService, Optional<ContinuousIntegrationService> continuousIntegrationService) {
this.exerciseRepository = exerciseRepository;
this.userService = userService;
this.participationService = participationService;
this.continuousIntegrationService = continuousIntegrationService;
}

/**
Expand Down Expand Up @@ -85,6 +85,21 @@ public Exercise findOne(Long id) {
return exercise;
}

/**
* Find exercise by id and load participations in this exercise.
*
* @param id the id of the exercise entity
* @return the exercise entity
*/
@Transactional(readOnly = true)
public Exercise findOneLoadParticipations(Long id) {
log.debug("Request to find Exercise with participations loaded: {}", id);
Exercise exercise = findOne(id);
if(Optional.ofNullable(exercise).isPresent()) {
exercise.getParticipations();
}
return exercise;
}

/**
* Resets an Exercise by deleting all its Participations
Expand Down Expand Up @@ -118,4 +133,26 @@ public void delete(Long id, boolean deleteParticipations) {
exerciseRepository.delete(id);
}

/**
* Delete build plans (except BASE) of all exercise participations.
*
* @param id id of the exercise for which build plans in respective participations are deleted
*/
@Transactional
public void deleteBuildPlans(Long id) {
log.debug("Request to delete build plans for Exercise : {}", id);
Exercise exercise = findOneLoadParticipations(id);
if (Optional.ofNullable(exercise).isPresent() && exercise instanceof ProgrammingExercise) {
exercise.getParticipations().forEach(participation -> {
if (participation.getBuildPlanId() != null) {
continuousIntegrationService.get().deleteBuildPlan(participation.getBuildPlanId());
participation.setInitializationState(ParticipationState.INACTIVE);
participation.setBuildPlanId(null);
participationService.save(participation);
}
});
} else {
log.debug("Exercise with id {} is not an instance of ProgrammingExercise. Ignoring the request to delete build plans", id);
}
}
}
Expand Up @@ -97,6 +97,23 @@ else if (exercise instanceof QuizExercise) {
return participation;
}

/**
* Service method to resume inactive participation (with previously deleted build plan)
* @param exercise exercise to which the inactive participation belongs
* @return resumed participation
*/
public Participation resume(Exercise exercise, Participation participation) {
ProgrammingExercise programmingExercise = (ProgrammingExercise) exercise;
participation = copyBuildPlan(participation, programmingExercise);
participation = configureBuildPlan(participation, programmingExercise);
participation.setInitializationState(ParticipationState.INITIALIZED);
participation.setInitializationDate(ZonedDateTime.now());

save(participation);
return participation;
}

private Participation copyRepository(Participation participation, ProgrammingExercise exercise) {
if (!participation.getInitializationState().hasCompletedState(ParticipationState.REPO_COPIED)) {
URL repositoryUrl = versionControlService.get().copyRepository(exercise.getBaseRepositoryUrlAsUrl(), participation.getStudent().getLogin());
Expand Down Expand Up @@ -180,16 +197,19 @@ public Participation findOne(Long id) {
}

/**
* Get one participation by its student and exercise.
* Get one initialized/inactive participation by its student and exercise.
*
* @param exerciseId the project key of the exercise
* @param username the username of the student
* @return the entity
*/
@Transactional(readOnly = true)
public Participation findOneByExerciseIdAndStudentLogin(Long exerciseId, String username) {
log.debug("Request to get Participation for User {} for Exercise with id: {}", username, exerciseId);
log.debug("Request to get initialized/inactive Participation for User {} for Exercise with id: {}", username, exerciseId);
Participation participation = participationRepository.findOneByExerciseIdAndStudentLoginAndInitializationState(exerciseId, username, ParticipationState.INITIALIZED);
if(!Optional.ofNullable(participation).isPresent()) {
participation = participationRepository.findOneByExerciseIdAndStudentLoginAndInitializationState(exerciseId, username, ParticipationState.INACTIVE);
}
return participation;
}

Expand Down
Expand Up @@ -208,4 +208,20 @@ public ResponseEntity<Void> resetExercise(@PathVariable Long id) {
exerciseService.reset(exercise);
return ResponseEntity.ok().headers(HeaderUtil.createEntityUpdateAlert("exercise", id.toString())).build();
}


/**
* DELETE /exercises/:id/buildplans : delete all build plans (except BASE) of all participations belonging to this exercise.
*
* @param id the id of the exercise to delete build plans for
* @return ResponseEntity with status
*/
@DeleteMapping(value = "/exercises/{id}/buildplans")
@PreAuthorize("hasAnyRole('ADMIN', 'TA')")
@Timed
public ResponseEntity<Void> deleteBuildPlansForExercise(@PathVariable Long id) {
log.debug("REST request to delete build plans for Exercise : {}", id);
exerciseService.deleteBuildPlans(id);
return ResponseEntity.ok().headers(HeaderUtil.createEntityUpdateAlert("exercise", id.toString())).build();
}
}
Expand Up @@ -3,6 +3,8 @@
import com.codahale.metrics.annotation.Timed;
import de.tum.in.www1.exerciseapp.domain.Exercise;
import de.tum.in.www1.exerciseapp.domain.Participation;
import de.tum.in.www1.exerciseapp.domain.ProgrammingExercise;
import de.tum.in.www1.exerciseapp.domain.enumeration.ParticipationState;
import de.tum.in.www1.exerciseapp.repository.ParticipationRepository;
import de.tum.in.www1.exerciseapp.security.AuthoritiesConstants;
import de.tum.in.www1.exerciseapp.service.ContinuousIntegrationService;
Expand Down Expand Up @@ -105,6 +107,30 @@ public ResponseEntity<Participation> initParticipation(@PathVariable Long course
}
}

/**
* POST /courses/:courseId/exercises/:exerciseId/resume-participation: resume the participation of the current user in the exercise identified by id
*
* @param courseId only included for API consistency, not actually used
* @param exerciseId id of the exercise for which to resume participation
* @param principal current user principal
* @return ResponseEntity with status 200 (OK) and with updated participation as a body, or with status 500 (Internal Server Error)
*/
@PutMapping(value = "/courses/{courseId}/exercises/{exerciseId}/resume-participation")
@PreAuthorize("hasAnyRole('USER', 'TA', 'ADMIN')")
@Timed
public ResponseEntity<Participation> resumeParticipation(@PathVariable Long courseId, @PathVariable Long exerciseId, Principal principal) throws URISyntaxException {
log.debug("REST request to resume Exercise : {}", exerciseId);
Exercise exercise = exerciseService.findOne(exerciseId);
Participation participation = participationService.findOneByExerciseIdAndStudentLogin(exerciseId, principal.getName());
if(exercise instanceof ProgrammingExercise) {
participation = participationService.resume(exercise, participation);
return ResponseEntity.ok().headers(HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, participation.getId().toString()))
.body(participation);
}
log.debug("Exercise with id {} is not an instance of ProgrammingExercise. Ignoring the request to resume participation", exerciseId);
return ResponseEntity.ok().body(participation);
}

/**
* PUT /participations : Updates an existing participation.
*
Expand Down
24 changes: 21 additions & 3 deletions src/main/webapp/app/courses/exercises/exercise-list.component.js
Expand Up @@ -29,9 +29,10 @@

vm.getClonePopoverTemplate = getClonePopoverTemplate;
vm.goToBuildPlan = goToBuildPlan;
vm.hasParticipation = hasParticipation;
vm.participationStatus = participationStatus;
vm.isReleased = isReleased;
vm.start = start;
vm.resume = resume;
vm.now = Date.now();
vm.numOfOverdueExercises = 0;
vm.showOverdueExercises = false;
Expand Down Expand Up @@ -110,8 +111,13 @@
});
}

function hasParticipation(exercise) {
return !angular.equals({}, exercise.participation.toJSON());
function participationStatus(exercise) {
if(angular.equals({}, exercise.participation)) {
return "uninitialized";
} else if(exercise.participation.initializationState === "INITIALIZED") {
return "initialized";
}
return "inactive";
}

function isReleased(exercise) {
Expand Down Expand Up @@ -165,6 +171,18 @@
});
}

function resume(exercise) {
vm.loading[exercise.id] = true;
exercise.$resume({
courseId: exercise.course.id,
exerciseId: exercise.id
}).catch(function(errorResponse) {
alert(errorResponse.data.status + " " + errorResponse.data.detail);
}).finally(function() {
vm.loading[exercise.id] = false;
});
}

function toggleShowOverdueExercises() {
vm.showOverdueExercises = true;
}
Expand Down
14 changes: 11 additions & 3 deletions src/main/webapp/app/courses/exercises/exercise-list.html
Expand Up @@ -18,7 +18,7 @@
</td>
</tr>

<tr ng-repeat="exercise in $ctrl.exercises | orderBy:'dueDate' | filter:$ctrl.isNotOverdue" ng-switch on="$ctrl.hasParticipation(exercise)">
<tr ng-repeat="exercise in $ctrl.exercises | orderBy:'dueDate' | filter:$ctrl.isNotOverdue" ng-switch on="$ctrl.participationStatus(exercise)">
<td>
<span has-authority="ROLE_USER">{{exercise.title}}</span>
<a class="text-primary" has-any-authority="ROLE_ADMIN, ROLE_TA"><i class="fa fa-info-circle fa-fw"
Expand All @@ -37,13 +37,21 @@
</td>
<td class="text-center">
<button class="btn btn-primary btn-sm btn-block" id="btn-student-action"
ng-switch-when="false"
ng-switch-when="uninitialized"
ng-click="$ctrl.start(exercise)"
ng-disabled="$ctrl.loading[exercise.id.toString()]">
<span ng-hide="$ctrl.loading[exercise.id.toString()]"><i class="fa fa-play-circle fa-fw"></i>&nbsp;Start exercise</span>
<i class="fa fa-circle-o-notch fa-spin" ng-show="$ctrl.loading[exercise.id.toString()]"></i>
</button>
<div ng-switch-when="true">
<button class="btn btn-primary btn-sm btn-block"
ng-switch-when="inactive"
ng-click="$ctrl.resume(exercise)"
ng-disabled="$ctrl.loading[exercise.id]"
>
<span ng-hide="$ctrl.loading[exercise.id]"><i class="fa fa-play-circle fa-fw"></i>&nbsp;Resume exercise</span>
<i ng-show="$ctrl.loading[exercise.id]" class="fa fa-circle-o-notch fa-spin"></i>
</button>
<div ng-switch-when="initialized">
<a ui-sref="editor({participationId:exercise.participation.id})" class="btn btn-primary btn-sm btn-block" id="btn-online-editor" ng-if="exercise.allowOnlineEditor"><i class="fa fa-folder-open fa-fw"></i>&nbsp;Open exercise</a>
<button uib-popover-html="::$ctrl.getClonePopoverTemplate(exercise)"
popover-placement="{{$ctrl.clonePopover.placement}}"
Expand Down
14 changes: 14 additions & 0 deletions src/main/webapp/app/entities/course/course.service.js
Expand Up @@ -60,6 +60,20 @@
return data;
},
ignoreLoadingBar: true
},
'resume': {
url: resourceUrl + '/resume-participation',
method: 'PUT',
transformResponse: function(data) {
data = angular.fromJson(data);
if(data.exercise) {
var exercise = data.exercise;
delete(data.exercise);
exercise.participation = data;
return exercise;
}
return data;
}
}
});
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/webapp/app/entities/exercise/exercise.service.js
Expand Up @@ -26,7 +26,8 @@
return data;
}
},
'update': {method: 'PUT'}
'update': {method: 'PUT'},
'deleteBuildPlans': {method: 'DELETE', url: resourceUrl + '/buildplans'}
});
}

Expand Down
@@ -0,0 +1,30 @@
(function () {
'use strict';

angular
.module('artemisApp')
.controller('BuildPlansDeleteController', BuildPlansDeleteController);

BuildPlansDeleteController.$inject = ['$uibModalInstance', 'entity', 'Exercise'];

function BuildPlansDeleteController($uibModalInstance, entity, Exercise) {
var vm = this;

vm.exercise = entity;
vm.clear = clear;
vm.confirmDelete = confirmDelete;

function clear () {
$uibModalInstance.dismiss('cancel');
}

function confirmDelete(id) {
Exercise.deleteBuildPlans({
id: id
},
function () {
$uibModalInstance.close(true);
});
}
}
})();
@@ -0,0 +1,22 @@
<form name="deleteForm" ng-submit="$ctrl.confirmDelete($ctrl.exercise.id)">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
ng-click="$ctrl.clear()">&times;</button>
<h4 class="modal-title" data-translate="entity.delete.title">Confirm delete operation</h4>
</div>
<div class="modal-body">
<jhi-alert-error></jhi-alert-error>
<p data-translate="artemisApp.instructorDashboard.buildPlans.delete.question"
translate-values="{exerciseTitle: '{{$ctrl.exercise.title}}', courseTitle: '{{$ctrl.exercise.course.title}}'}">CONFIRM_DELETE
</p>
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="$ctrl.clear()">
<span class="glyphicon glyphicon-ban-circle"></span>&nbsp;<span data-translate="entity.action.cancel">CANCEL</span>
</button>
<button class="btn btn-danger">
<span class="glyphicon glyphicon-remove-circle"></span>&nbsp;<span data-translate="entity.action.delete">DELETE</span>
</button>
</div>
</form>
Expand Up @@ -29,6 +29,7 @@
vm.showDetails = showDetails;
vm.sort = sort;
vm.toggleShowAllResults = toggleShowAllResults;
vm.buildPlanCleanup = buildPlanCleanup;
vm.exercise = Exercise.get({id : vm.exerciseId});

function init() {
Expand All @@ -39,6 +40,10 @@
return $filter('amDifference')(completionDate, initializationDate, 'minutes');
}

function buildPlanCleanup() {
console.log("cleanup called");
}

function exportData() {
if (vm.sortedResults.length > 0) {
var rows = [];
Expand Down
@@ -1,9 +1,13 @@

<h2>{{$ctrl.exercise.title}} <small>{{$ctrl.results.length}} results</small></h2>


<jhi-alert></jhi-alert>
<div>
<div class="button-toolbar pull-right">
<!--ng-click="$ctrl.buildPlanCleanup()"-->
<button class="btn btn-primary btn-sm" ui-sref="instructor-dashboard.buildplans-delete"
data-translate="artemisApp.instructorDashboard.buildPlans.delete.title">
<i class="fa fa-eraser" aria-hidden="true"></i>&nbsp;CLEANUP_BUILD_PLANS
</button>
<button type="submit"
ui-sref="participation-for-exercise({exerciseId:$ctrl.exerciseId})"
class="btn btn-primary btn-sm">
Expand Down

0 comments on commit 43d1f12

Please sign in to comment.