Skip to content

Commit

Permalink
Merge branch 'develop' into bugfix/fix-lti-quiz-exercise-grade
Browse files Browse the repository at this point in the history
  • Loading branch information
basak-akan authored Nov 8, 2023
2 parents 85704ee + 37872e4 commit 146e465
Show file tree
Hide file tree
Showing 18 changed files with 152 additions and 23 deletions.
31 changes: 31 additions & 0 deletions docs/admin/setup/distributed.rst
Original file line number Diff line number Diff line change
Expand Up @@ -502,3 +502,34 @@ It relays message between instances:

.. figure:: distributed/registry.png
:align: center


Running multiple instances locally
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For testing purposes, you can also run multiple instances on the same machine. You can do this by using
different ports and a unique instance ID for each instance.

#. In ``application-local.yml``, add the following configuration:

.. code:: yaml
eureka:
client:
enabled: true
#. Create additional run configurations for each instance. You will have to add CLI arguments to each additional run
configuration to set the instance ID and the port, e.g. ``--server.port=8081 --eureka.instance.instanceId="Artemis:2"``.
Also, make sure that only one instance has the ``scheduling`` profile enabled:

.. figure:: distributed/run-config.png
:align: center


#. Start the registry service, e.g., by running ``docker compose -f docker/broker-registry.yml up``.

#. Start the first instance with the default run configuration (no additional CLI arguments, ``scheduling`` enabled)
and wait until it is up and running.

#. Start the remaining instances.

You should now be able to see all instances in the registry interface at ``http://localhost:8761``.
Binary file added docs/admin/setup/distributed/run-config.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ public interface TutorialGroupRepository extends JpaRepository<TutorialGroup, Lo
@Query("""
SELECT DISTINCT tutorialGroup.campus
FROM TutorialGroup tutorialGroup
WHERE tutorialGroup.course.id = :#{#courseId} AND tutorialGroup.campus IS NOT NULL""")
Set<String> findAllUniqueCampusValuesInCourse(@Param("courseId") Long courseId);
WHERE tutorialGroup.course.instructorGroupName IN (:#{#userGroups}) AND tutorialGroup.campus IS NOT NULL""")
Set<String> findAllUniqueCampusValuesInRegisteredCourse(@Param("userGroups") Set<String> userGroups);

@Query("""
SELECT DISTINCT tutorialGroup.language
FROM TutorialGroup tutorialGroup
WHERE tutorialGroup.course.id = :#{#courseId} AND tutorialGroup.language IS NOT NULL""")
Set<String> findAllUniqueLanguageValuesInCourse(@Param("courseId") Long courseId);
WHERE tutorialGroup.course.instructorGroupName IN (:#{#userGroups}) AND tutorialGroup.language IS NOT NULL""")
Set<String> findAllUniqueLanguageValuesInRegisteredCourse(@Param("userGroups") Set<String> userGroups);

@Query("""
SELECT tutorialGroup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import de.tum.in.www1.artemis.domain.scores.ParticipantScore;
import de.tum.in.www1.artemis.repository.*;
import de.tum.in.www1.artemis.web.rest.dto.ScoreDTO;
import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;

@Service
public class ParticipantScoreService {
Expand Down Expand Up @@ -116,7 +117,8 @@ private List<ScoreDTO> calculateScores(Set<Exercise> exercises, Set<User> users,
Set<Exercise> individualExercises = exercises.stream().filter(exercise -> !exercise.isTeamMode()).collect(Collectors.toSet());
Set<Exercise> teamExercises = exercises.stream().filter(Exercise::isTeamMode).collect(Collectors.toSet());

Course course = exercises.stream().findAny().orElseThrow().getCourseViaExerciseGroupOrCourseMember();
Course course = exercises.stream().findAny().orElseThrow(() -> new EntityNotFoundException("The result you are referring to does not exist"))
.getCourseViaExerciseGroupOrCourseMember();

// For every student we want to calculate the score
Map<Long, ScoreDTO> userIdToScores = users.stream().collect(Collectors.toMap(User::getId, user -> new ScoreDTO(user.getId(), user.getLogin(), 0.0, 0.0, 0.0)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public ResponseEntity<List<ScoreDTO>> getScoresOfCourse(@PathVariable Long cours
* has been battle tested enough.
*
* @param examId the id of the exam for which to calculate the exam scores
* @return list of scores for every registered user in the xam
* @return list of scores for every registered user in the exam or 404 not found if scores are empty
*/
@GetMapping("/exams/{examId}/exam-scores")
@EnforceAtLeastInstructor
Expand All @@ -88,6 +88,6 @@ public ResponseEntity<List<ScoreDTO>> getScoresOfExam(@PathVariable Long examId)
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, exam.getCourse(), null);
List<ScoreDTO> scoreDTOS = participantScoreService.calculateExamScores(exam);
log.info("getScoresOfExam took {}ms", System.currentTimeMillis() - start);
return ResponseEntity.ok().body(scoreDTOS);
return scoreDTOS.isEmpty() ? ResponseEntity.notFound().build() : ResponseEntity.ok().body(scoreDTOS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,37 +116,39 @@ public ResponseEntity<String> getTitle(@PathVariable Long tutorialGroupId) {
}

/**
* GET /courses/:courseId/tutorial-groups/campus-values : gets the campus values used for the tutorial groups of the course with the given id
* GET /courses/:courseId/tutorial-groups/campus-values : gets the campus values used for the tutorial groups of all tutorials where user is instructor
* Note: Used for autocomplete in the client tutorial form
*
* @param courseId the id of the course to which the tutorial groups belong to
* @return ResponseEntity with status 200 (OK) and with body containing the unique campus values of the tutorial groups of the course
* @return ResponseEntity with status 200 (OK) and with body containing the unique campus values of all tutorials where user is instructor
*/
@GetMapping("/courses/{courseId}/tutorial-groups/campus-values")
@EnforceAtLeastInstructor
@FeatureToggle(Feature.TutorialGroups)
public ResponseEntity<Set<String>> getUniqueCampusValues(@PathVariable Long courseId) {
log.debug("REST request to get unique campus values used for tutorial groups in course : {}", courseId);
var course = courseRepository.findByIdElseThrow(courseId);
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null);
return ResponseEntity.ok(tutorialGroupRepository.findAllUniqueCampusValuesInCourse(courseId));
var user = userRepository.getUserWithGroupsAndAuthorities();
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, user);
return ResponseEntity.ok(tutorialGroupRepository.findAllUniqueCampusValuesInRegisteredCourse(user.getGroups()));
}

/**
* GET /courses/:courseId/tutorial-groups/language-values : gets the language values used for the tutorial groups of the course with the given id
* GET /courses/:courseId/tutorial-groups/language-values : gets the language values used for the tutorial groups of all tutorials where user is instructor
* Note: Used for autocomplete in the client tutorial form
*
* @param courseId the id of the course to which the tutorial groups belong to
* @return ResponseEntity with status 200 (OK) and with body containing the unique language values of the tutorial groups of the course
* @return ResponseEntity with status 200 (OK) and with body containing the unique language values of all tutorials where user is instructor
*/
@GetMapping("/courses/{courseId}/tutorial-groups/language-values")
@EnforceAtLeastInstructor
@FeatureToggle(Feature.TutorialGroups)
public ResponseEntity<Set<String>> getUniqueLanguageValues(@PathVariable Long courseId) {
log.debug("REST request to get unique language values used for tutorial groups in course : {}", courseId);
var course = courseRepository.findByIdElseThrow(courseId);
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null);
return ResponseEntity.ok(tutorialGroupRepository.findAllUniqueLanguageValuesInCourse(courseId));
var user = userRepository.getUserWithGroupsAndAuthorities();
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, user);
return ResponseEntity.ok(tutorialGroupRepository.findAllUniqueLanguageValuesInRegisteredCourse(user.getGroups()));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export class CreateTutorialGroupComponent implements OnInit, OnDestroy {

createTutorialGroup(formData: TutorialGroupFormData) {
const { title, teachingAssistant, additionalInformation, capacity, isOnline, language, campus, schedule } = formData;

this.tutorialGroupToCreate.title = title;
this.tutorialGroupToCreate.teachingAssistant = teachingAssistant;
this.tutorialGroupToCreate.additionalInformation = additionalInformation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export class TutorialGroupFormComponent implements OnInit, OnChanges, OnDestroy
teachingAssistant: [undefined, [Validators.required]],
capacity: [undefined, [Validators.min(1)]],
isOnline: [false, [Validators.required]],
language: ['German', [Validators.required, Validators.maxLength(255)]],
language: [undefined, [Validators.required, Validators.maxLength(255)]],
campus: [undefined, Validators.maxLength(255)],
});

Expand Down Expand Up @@ -367,6 +367,13 @@ export class TutorialGroupFormComponent implements OnInit, OnChanges, OnDestroy
),
).subscribe((languages: string[]) => {
this.languages = languages;
// default values for English & German
if (!languages.includes('English')) {
this.languages.push('English');
}
if (!languages.includes('German')) {
this.languages.push('German');
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
<span class="sr-only">Loading...</span>
</div>
</div>
<div *ngIf="!isLoading && (!studentResults || !exerciseGroups)">
<div class="alert alert-warning" *ngIf="!exerciseGroups">
<fa-icon [icon]="faExclamationTriangle"></fa-icon>
<span>{{ 'artemisApp.examScores.noExerciseGroupAvailable' | artemisTranslate }}</span>
</div>
<div class="alert alert-info" *ngIf="!studentResults">
<span>{{ 'artemisApp.examScores.noStudentResultAvailable' | artemisTranslate }}</span>
</div>
</div>

<div *ngIf="!isLoading && studentResults && exerciseGroups">
<div class="d-flex">
Expand Down
5 changes: 3 additions & 2 deletions src/main/webapp/app/exam/exam-scores/exam-scores.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { declareExerciseType } from 'app/entities/exercise.model';
import { mean, median, standardDeviation } from 'simple-statistics';
import { CourseManagementService } from 'app/course/manage/course-management.service';
import { ButtonSize } from 'app/shared/components/button.component';
import { faCheckCircle, faDownload, faSort, faTimes } from '@fortawesome/free-solid-svg-icons';
import { faCheckCircle, faDownload, faExclamationTriangle, faSort, faTimes } from '@fortawesome/free-solid-svg-icons';
import { Course } from 'app/entities/course.model';
import { CsvExportRowBuilder } from 'app/shared/export/csv-export-row-builder';
import { ExcelExportRowBuilder } from 'app/shared/export/excel-export-row-builder';
Expand Down Expand Up @@ -127,6 +127,7 @@ export class ExamScoresComponent implements OnInit, OnDestroy {
faDownload = faDownload;
faTimes = faTimes;
faCheckCircle = faCheckCircle;
faExclamationTriangle = faExclamationTriangle;

private languageChangeSubscription?: Subscription;
constructor(
Expand All @@ -146,7 +147,7 @@ export class ExamScoresComponent implements OnInit, OnDestroy {
this.route.params.subscribe((params) => {
const getExamScoresObservable = this.examService.getExamScores(params['courseId'], params['examId']);
// alternative exam scores calculation using participant scores table
const findExamScoresObservable = this.participantScoresService.findExamScores(params['examId']);
const findExamScoresObservable = this.participantScoresService.findExamScores(params['examId']).pipe(catchError(() => of(new HttpResponse<ScoresDTO[]>())));

// find grading scale if one exists and handle case when it doesn't
const gradingScaleObservable = this.gradingSystemService
Expand Down
13 changes: 11 additions & 2 deletions src/main/webapp/app/shared/metis/post.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,20 @@ export class PostService extends PostingService<Post> {
return res;
}

/**
* Determines whether to use the /messages or /posts API endpoints based on the presence of certain properties in the postContextFilter or post objects.
* If any properties related to conversations are present (conversation, conversationId, or courseWideChannelIds), it returns /messages.
* Otherwise, it defaults to /posts.
*
* @param postContextFilter current post context filter in use
* @param post new or updated post
* @return '/messages' or '/posts'
*/
private static getResourceEndpoint(postContextFilter?: PostContextFilter, post?: Post): string {
if (post?.conversation || postContextFilter?.conversationId) {
if (post?.conversation || postContextFilter?.conversationId || postContextFilter?.courseWideChannelIds) {
return '/messages';
} else {
return '/messages';
return '/posts';
}
}
}
4 changes: 3 additions & 1 deletion src/main/webapp/i18n/de/exam.json
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@
"totalColumn": "Total",
"exerciseGroupTitle": "Durchschnittliche Ergebnisse der Aufgabengruppe \"{{ groupTitle }}\" im Vergleich",
"gradesBeforeBonus": "Noten vor Bonus",
"gradesAfterBonus": "Noten nach Bonus"
"gradesAfterBonus": "Noten nach Bonus",
"noStudentResultAvailable": "Keine Studentenabgabe vorhanden",
"noExerciseGroupAvailable": "Keine Aufgabengruppen vorhanden"
},
"examParticipation": {
"timer": "Restzeit: ",
Expand Down
4 changes: 3 additions & 1 deletion src/main/webapp/i18n/en/exam.json
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@
"totalColumn": "Total",
"exerciseGroupTitle": "Average scores of exercise group \"{{ groupTitle }}\" in comparison",
"gradesBeforeBonus": "Grades before bonus",
"gradesAfterBonus": "Grades after bonus"
"gradesAfterBonus": "Grades after bonus",
"noStudentResultAvailable": "No student results available",
"noExerciseGroupAvailable": "No exercise groups available"
},
"examParticipation": {
"timer": "Time left: ",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,13 @@ describe('FileUploadExercise Management Component', () => {
expect(comp.filteredFileUploadExercises).toHaveLength(0);
});
});

it('should have working selection', () => {
// WHEN
comp.toggleExercise(fileUploadExercise);

// THEN
expect(comp.selectedExercises[0]).toContainEntry(['id', fileUploadExercise.id]);
expect(comp.allChecked).toEqual(comp.selectedExercises.length === comp.fileUploadExercises.length);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,13 @@ describe('ModelingExercise Management Component', () => {
expect(sortSpy).toHaveBeenCalledWith(comp.modelingExercises, comp.predicate, comp.reverse);
expect(sortSpy).toHaveBeenCalledOnce();
});

it('should have working selection', () => {
// WHEN
comp.toggleExercise(modelingExercise);

// THEN
expect(comp.selectedExercises[0]).toContainEntry(['id', modelingExercise.id]);
expect(comp.allChecked).toEqual(comp.selectedExercises.length === comp.modelingExercises.length);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,12 @@ describe('QuizExercise Management Component', () => {
expect(comp.filteredQuizExercises).toHaveLength(0);
});
});
it('should have working selection', () => {
// WHEN
comp.toggleExercise(quizExercise);

// THEN
expect(comp.selectedExercises[0]).toContainEntry(['id', quizExercise.id]);
expect(comp.allChecked).toEqual(comp.selectedExercises.length === comp.quizExercises.length);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,13 @@ describe('TextExercise Management Component', () => {
expect(comp.filteredTextExercises).toHaveLength(0);
});
});

it('should have working selection', () => {
// WHEN
comp.toggleExercise(textExercise);

// THEN
expect(comp.selectedExercises[0]).toContainEntry(['id', textExercise.id]);
expect(comp.allChecked).toEqual(comp.selectedExercises.length === comp.textExercises.length);
});
});
30 changes: 30 additions & 0 deletions src/test/javascript/spec/service/metis/post.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,36 @@ describe('Post Service', () => {
req.flush(returnedFromService);
tick();
}));

it('should use /posts endpoints if plagiarismCaseId is provided in the postContextFilter', fakeAsync(() => {
const plagiarismCaseId = 123;
const expectedUrl = `${service.resourceUrl}${metisCourse.id}/posts?plagiarismCaseId=${plagiarismCaseId}`;
const mockResponse: Post[] = [];

service
.getPosts(metisCourse.id!, { plagiarismCaseId })
.pipe(take(1))
.subscribe((resp) => expect(resp.body).toEqual(mockResponse));
const req = httpMock.expectOne({ method: 'GET', url: expectedUrl });

req.flush(mockResponse);
tick();
}));

it('should use /messages endpoints if course-wide channel ids are provided', fakeAsync(() => {
const courseWideChannelIds = [123];
const expectedUrl = `${service.resourceUrl}${metisCourse.id}/messages?courseWideChannelIds=${courseWideChannelIds}`;
const mockResponse: Post[] = [];

service
.getPosts(metisCourse.id!, { courseWideChannelIds })
.pipe(take(1))
.subscribe((resp) => expect(resp.body).toEqual(mockResponse));
const req = httpMock.expectOne({ method: 'GET', url: expectedUrl });

req.flush(mockResponse);
tick();
}));
});

afterEach(() => {
Expand Down

0 comments on commit 146e465

Please sign in to comment.