diff --git a/src/main/java/de/tum/in/www1/artemis/domain/BaseExercise.java b/src/main/java/de/tum/in/www1/artemis/domain/BaseExercise.java index 10851004a19c..3e0206eca6d5 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/BaseExercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/BaseExercise.java @@ -191,9 +191,9 @@ public void setExampleSolutionPublicationDate(@Nullable ZonedDateTime exampleSol * @return true, if students are allowed to see this exercise, otherwise false */ @JsonView(QuizView.Before.class) - public Boolean isVisibleToStudents() { + public boolean isVisibleToStudents() { if (releaseDate == null) { // no release date means the exercise is visible to students - return Boolean.TRUE; + return true; } return releaseDate.isBefore(ZonedDateTime.now()); } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java index b3e09a3401e6..1cb35f084a28 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java @@ -467,7 +467,7 @@ else if (participation.getExercise() instanceof ModelingExercise || participatio * @return the latest relevant result in the given participation, or null, if none exist */ @Nullable - public Submission findLatestSubmissionWithRatedResultWithCompletionDate(Participation participation, Boolean ignoreAssessmentDueDate) { + public Submission findLatestSubmissionWithRatedResultWithCompletionDate(Participation participation, boolean ignoreAssessmentDueDate) { // for most types of exercises => return latest result (all results are relevant) Submission latestSubmission = null; // we get the results over the submissions diff --git a/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizExercise.java b/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizExercise.java index 8349f6071604..6234368be221 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizExercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizExercise.java @@ -164,7 +164,7 @@ public String getType() { * @return true if quiz has started, false otherwise */ @JsonView(QuizView.Before.class) - public Boolean isQuizStarted() { + public boolean isQuizStarted() { return isVisibleToStudents(); } @@ -174,7 +174,7 @@ public Boolean isQuizStarted() { * @return true if quiz has ended, false otherwise */ @JsonView(QuizView.Before.class) - public Boolean isQuizEnded() { + public boolean isQuizEnded() { return getDueDate() != null && ZonedDateTime.now().isAfter(getDueDate()); } @@ -184,7 +184,7 @@ public Boolean isQuizEnded() { * @return true if quiz should be filtered, false otherwise */ @JsonIgnore - public Boolean shouldFilterForStudents() { + public boolean shouldFilterForStudents() { return !isQuizEnded(); } @@ -194,7 +194,7 @@ public Boolean shouldFilterForStudents() { * @return true if the quiz is valid, otherwise false */ @JsonIgnore - public Boolean isValid() { + public boolean isValid() { // check title if (getTitle() == null || getTitle().isEmpty()) { return false; @@ -353,7 +353,7 @@ public StudentParticipation findRelevantParticipation(List @Override @Nullable - public Submission findLatestSubmissionWithRatedResultWithCompletionDate(Participation participation, Boolean ignoreAssessmentDueDate) { + public Submission findLatestSubmissionWithRatedResultWithCompletionDate(Participation participation, boolean ignoreAssessmentDueDate) { // The shouldFilterForStudents() method uses the exercise release/due dates, not the ones of the exam, therefor we can only use them if this exercise is not part of an exam // In exams, all results should be seen as relevant as they will only be created once the exam is over if (shouldFilterForStudents() && !isExamExercise()) { @@ -712,7 +712,7 @@ else if (batch != null && batch.isSubmissionAllowed()) { public void validateDates() { super.validateDates(); quizBatches.forEach(quizBatch -> { - if (quizBatch.getStartTime().isBefore(getReleaseDate())) { + if (quizBatch.getStartTime() != null && quizBatch.getStartTime().isBefore(getReleaseDate())) { throw new BadRequestAlertException("Start time must not be before release date!", getTitle(), "noValidDates"); } }); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java index 9a5935845288..4fc853b8cd71 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java @@ -58,44 +58,43 @@ public interface CourseRepository extends JpaRepository { """) List findAllActive(@Param("now") ZonedDateTime now); + // Note: you should not add exercises or exercises+categories here, because this would make the query too complex and would take significantly longer @EntityGraph(type = LOAD, attributePaths = { "lectures", "lectures.attachments", "exams" }) @Query(""" - SELECT DISTINCT c FROM Course c - WHERE (c.startDate <= :now - OR c.startDate IS NULL) - AND (c.endDate >= :now - OR c.endDate IS NULL) + SELECT DISTINCT c + FROM Course c + WHERE (c.startDate <= :now OR c.startDate IS NULL) + AND (c.endDate >= :now OR c.endDate IS NULL) """) List findAllActiveWithLecturesAndExams(@Param("now") ZonedDateTime now); + // Note: you should not add exercises or exercises+categories here, because this would make the query too complex and would take significantly longer + @EntityGraph(type = LOAD, attributePaths = { "lectures", "lectures.attachments", "exams" }) + Optional findWithLecturesAndExamsById(long courseId); + @Query(""" - SELECT DISTINCT c FROM Course c - LEFT JOIN FETCH c.tutorialGroups tutorialGroups - LEFT JOIN FETCH tutorialGroups.teachingAssistant tutor - LEFT JOIN FETCH tutorialGroups.registrations registrations - LEFT JOIN FETCH registrations.student student - WHERE (c.startDate <= :#{#now} - OR c.startDate IS NULL) - AND (c.endDate >= :#{#now} - OR c.endDate IS NULL) - AND (student.id = :#{#userId} OR tutor.id = :#{#userId}) + SELECT DISTINCT c + FROM Course c + LEFT JOIN FETCH c.tutorialGroups tutorialGroups + LEFT JOIN FETCH tutorialGroups.teachingAssistant tutor + LEFT JOIN FETCH tutorialGroups.registrations registrations + LEFT JOIN FETCH registrations.student student + WHERE (c.startDate <= :now OR c.startDate IS NULL) + AND (c.endDate >= :now OR c.endDate IS NULL) + AND (student.id = :userId OR tutor.id = :userId) """) List findAllActiveWithTutorialGroupsWhereUserIsRegisteredOrTutor(@Param("now") ZonedDateTime now, @Param("userId") Long userId); - @EntityGraph(type = LOAD, attributePaths = { "lectures", "lectures.attachments", "exams" }) - Optional findWithEagerLecturesAndExamsById(long courseId); - // Note: this is currently only used for testing purposes @Query(""" - SELECT DISTINCT c FROM Course c - LEFT JOIN FETCH c.exercises exercises - LEFT JOIN FETCH c.lectures lectures - LEFT JOIN FETCH lectures.attachments + SELECT DISTINCT c + FROM Course c + LEFT JOIN FETCH c.exercises exercises + LEFT JOIN FETCH c.lectures lectures + LEFT JOIN FETCH lectures.attachments LEFT JOIN FETCH exercises.categories - WHERE (c.startDate <= :now - OR c.startDate IS NULL) - AND (c.endDate >= :now - OR c.endDate IS NULL) + WHERE (c.startDate <= :now OR c.startDate IS NULL) + AND (c.endDate >= :now OR c.endDate IS NULL) """) List findAllActiveWithEagerExercisesAndLectures(@Param("now") ZonedDateTime now); @@ -283,7 +282,7 @@ default List findAllCurrentlyActiveNotOnlineAndRegistrationEnabledWithOr */ @NotNull default Course findByIdWithLecturesAndExamsElseThrow(long courseId) { - return findWithEagerLecturesAndExamsById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); + return findWithLecturesAndExamsById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java index 5c18349e7a4b..42d25e1387ab 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java @@ -28,35 +28,48 @@ public interface ExerciseRepository extends JpaRepository { @Query(""" - SELECT e FROM Exercise e - LEFT JOIN FETCH e.categories - WHERE e.course.id = :#{#courseId} + SELECT e + FROM Exercise e + LEFT JOIN FETCH e.categories + WHERE e.course.id = :courseId """) Set findByCourseIdWithCategories(@Param("courseId") Long courseId); @Query(""" - SELECT e - FROM Exercise e LEFT JOIN FETCH e.categories WHERE - e.id IN :exerciseIds + SELECT e + FROM Exercise e + LEFT JOIN FETCH e.categories + WHERE e.course.id IN :courseIds """) - Set findByExerciseIdWithCategories(@Param("exerciseIds") Set exerciseIds); + Set findByCourseIdsWithCategories(@Param("courseIds") Set courseIds); @Query(""" - SELECT e FROM Exercise e - WHERE e.course.id = :#{#courseId} + SELECT e + FROM Exercise e + LEFT JOIN FETCH e.categories + WHERE e.id IN :exerciseIds + """) + Set findByExerciseIdsWithCategories(@Param("exerciseIds") Set exerciseIds); + + @Query(""" + SELECT e + FROM Exercise e + WHERE e.course.id = :courseId AND e.mode = 'TEAM' """) Set findAllTeamExercisesByCourseId(@Param("courseId") Long courseId); @Query(""" - SELECT e FROM Exercise e - WHERE e.course.id = :#{#courseId} + SELECT e + FROM Exercise e + WHERE e.course.id = :courseId """) Set findAllExercisesByCourseId(@Param("courseId") Long courseId); @Query(""" - SELECT e FROM Exercise e - LEFT JOIN FETCH e.learningGoals + SELECT e + FROM Exercise e + LEFT JOIN FETCH e.learningGoals WHERE e.id = :exerciseId """) Optional findByIdWithLearningGoals(@Param("exerciseId") Long exerciseId); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/QuizBatchRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/QuizBatchRepository.java index 8d50a29bd2c3..4fe1f25cb1cd 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/QuizBatchRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/QuizBatchRepository.java @@ -40,8 +40,8 @@ public interface QuizBatchRepository extends JpaRepository { FROM QuizBatch quizBatch JOIN QuizSubmission submission ON quizBatch.id = submission.quizBatch JOIN TREAT(submission.participation AS StudentParticipation) participation - WHERE participation.exercise.id = :#{#quizExercise.id} - AND participation.student.login = :#{#studentLogin} + WHERE participation.exercise = :quizExercise + AND participation.student.login = :studentLogin """) Set findAllByQuizExerciseAndStudentLogin(@Param("quizExercise") QuizExercise quizExercise, @Param("studentLogin") String studentLogin); diff --git a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java index dcc1c19983bb..1b20773756a2 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java @@ -29,7 +29,6 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; -import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; import de.tum.in.www1.artemis.domain.enumeration.NotificationType; import de.tum.in.www1.artemis.domain.exam.Exam; @@ -50,6 +49,7 @@ import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; import de.tum.in.www1.artemis.service.tutorialgroups.TutorialGroupService; import de.tum.in.www1.artemis.service.user.UserService; +import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.dto.CourseManagementDetailViewDTO; import de.tum.in.www1.artemis.web.rest.dto.DueDateStat; import de.tum.in.www1.artemis.web.rest.dto.StatsForDashboardDTO; @@ -187,9 +187,8 @@ public CourseService(Environment env, ArtemisAuthenticationProvider artemisAuthe * * @param courses the courses for which the participations should be fetched * @param user the user for which the participations should be fetched - * @param startTimeInMillis start time for logging purposes */ - public void fetchParticipationsWithSubmissionsAndResultsForCourses(List courses, User user, long startTimeInMillis) { + public void fetchParticipationsWithSubmissionsAndResultsForCourses(List courses, User user) { Set exercises = courses.stream().flatMap(course -> course.getExercises().stream()).collect(Collectors.toSet()); List participationsOfUserInExercises = studentParticipationRepository.getAllParticipationsOfUserInExercises(user, exercises); if (participationsOfUserInExercises.isEmpty()) { @@ -206,11 +205,6 @@ public void fetchParticipationsWithSubmissionsAndResultsForCourses(List } } } - Map> exercisesGroupedByExerciseMode = exercises.stream().collect(Collectors.groupingBy(Exercise::getMode)); - int noOfIndividualExercises = Objects.requireNonNullElse(exercisesGroupedByExerciseMode.get(ExerciseMode.INDIVIDUAL), List.of()).size(); - int noOfTeamExercises = Objects.requireNonNullElse(exercisesGroupedByExerciseMode.get(ExerciseMode.TEAM), List.of()).size(); - log.info("/courses/for-dashboard.done in {}ms for {} courses with {} individual exercises and {} team exercises for user {}", - System.currentTimeMillis() - startTimeInMillis, courses.size(), noOfIndividualExercises, noOfTeamExercises, user.getLogin()); } /** @@ -225,7 +219,10 @@ public Course findOneWithExercisesAndLecturesAndExamsAndLearningGoalsAndTutorial if (!authCheckService.isAtLeastStudentInCourse(course, user)) { throw new AccessForbiddenException(); } - course.setExercises(exerciseService.findAllForCourse(course, user)); + // Load exercises with categories separately because this is faster than loading them with lectures and exam above (the query would become too complex) + course.setExercises(exerciseRepository.findByCourseIdWithCategories(course.getId())); + course.setExercises(exerciseService.filterExercisesForCourse(course, user)); + exerciseService.loadExerciseDetailsIfNecessary(course, user); course.setLectures(lectureService.filterActiveAttachments(course.getLectures(), user)); course.setLearningGoals(learningGoalService.findAllForCourse(course, user)); course.setPrerequisites(learningGoalService.findAllPrerequisitesForCourse(course, user)); @@ -255,21 +252,35 @@ public List findAllActiveForUser(User user) { * @return an unmodifiable list of all courses including exercises, lectures and exams for the user */ public List findAllActiveWithExercisesAndLecturesAndExamsForUser(User user) { + long start = System.nanoTime(); var userVisibleCourses = courseRepository.findAllActiveWithLecturesAndExams().stream() // remove old courses that have already finished .filter(course -> course.getEndDate() == null || course.getEndDate().isAfter(ZonedDateTime.now())) // remove courses the user should not be able to see - .filter(course -> isCourseVisibleForUser(user, course)); + .filter(course -> isCourseVisibleForUser(user, course)).toList(); + + log.debug("Find user visible courses finished after {}", TimeLogUtil.formatDurationFrom(start)); - // TODO: find all exercises for all courses of the user in one db call to improve the performance, then we would need to map them to the correct course + long startFindAllExercises = System.nanoTime(); + var courseIds = userVisibleCourses.stream().map(DomainObject::getId).collect(Collectors.toSet()); + Set allExercises = exerciseRepository.findByCourseIdsWithCategories(courseIds); + log.debug("findAllExercisesByCourseIdsWithCategories finished with {} exercises after {}", allExercises.size(), TimeLogUtil.formatDurationFrom(startFindAllExercises)); - return userVisibleCourses.peek(course -> { - course.setExercises(exerciseService.findAllForCourse(course, user)); + long startFilterAll = System.nanoTime(); + var courses = userVisibleCourses.stream().peek(course -> { + // connect the exercises with the course + course.setExercises(allExercises.stream().filter(ex -> ex.getCourseViaExerciseGroupOrCourseMember().getId().equals(course.getId())).collect(Collectors.toSet())); + course.setExercises(exerciseService.filterExercisesForCourse(course, user)); + exerciseService.loadExerciseDetailsIfNecessary(course, user); course.setLectures(lectureService.filterActiveAttachments(course.getLectures(), user)); if (authCheckService.isOnlyStudentInCourse(course, user)) { course.setExams(examRepository.filterVisibleExams(course.getExams())); } }).toList(); + + log.debug("all {} filterExercisesForCourse individually finished together after {}", courses.size(), TimeLogUtil.formatDurationFrom(startFilterAll)); + log.debug("Filter exercises, lectures, and exams finished after {}", TimeLogUtil.formatDurationFrom(start)); + return courses; } private boolean isCourseVisibleForUser(User user, Course course) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java b/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java index 4aa3f3b57271..069008d5b99a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java @@ -253,7 +253,7 @@ public Set loadExercisesWithInformationForDashboard(Set exercise if (exerciseIds.isEmpty()) { return new HashSet<>(); } - Set exercises = exerciseRepository.findByExerciseIdWithCategories(exerciseIds); + Set exercises = exerciseRepository.findByExerciseIdsWithCategories(exerciseIds); // Set is needed here to remove duplicates Set courses = exercises.stream().map(Exercise::getCourseViaExerciseGroupOrCourseMember).collect(Collectors.toSet()); if (courses.size() != 1) { @@ -275,52 +275,55 @@ public Set loadExercisesWithInformationForDashboard(Set exercise } /** - * Finds all Exercises for a given Course + * Filter all exercises for a given course based on the user role and course settings + * Assumes that the exercises are already been loaded (i.e. no proxy) * - * @param course corresponding course + * @param course corresponding course: exercises * @param user the user entity - * @return a List of all Exercises for the given course + * @return a set of all Exercises for the given course */ - public Set findAllForCourse(Course course, User user) { - Set exercises = null; + public Set filterExercisesForCourse(Course course, User user) { + Set exercises = course.getExercises(); if (authCheckService.isAtLeastTeachingAssistantInCourse(course, user)) { - // tutors/instructors/admins can see all exercises of the course - exercises = exerciseRepository.findByCourseIdWithCategories(course.getId()); + // no need to filter for tutors/editors/instructors/admins because they can see all exercises of the course + return exercises; } - else if (authCheckService.isOnlyStudentInCourse(course, user)) { - - if (course.isOnlineCourse()) { - // students in online courses can only see exercises where the lti outcome url exists, otherwise the result cannot be reported later on - exercises = exerciseRepository.findByCourseIdWhereLtiOutcomeUrlExists(course.getId(), user.getLogin()); - } - else { - exercises = exerciseRepository.findByCourseIdWithCategories(course.getId()); - } - // students for this course might not have the right to see it, so we have to - // filter out exercises that are not released (or explicitly made visible to students) yet - exercises = exercises.stream().filter(Exercise::isVisibleToStudents).collect(Collectors.toSet()); + if (course.isOnlineCourse()) { + // this case happens rarely, so we can reload the relevant exercises from the database + // students in online courses can only see exercises where the lti outcome url exists, otherwise the result cannot be reported later on + exercises = exerciseRepository.findByCourseIdWhereLtiOutcomeUrlExists(course.getId(), user.getLogin()); } - if (exercises != null) { - for (Exercise exercise : exercises) { - setAssignedTeamIdForExerciseAndUser(exercise, user); + // students for this course might not have the right to see it, so we have to + // filter out exercises that are not released (or explicitly made visible to students) yet + return exercises.stream().filter(Exercise::isVisibleToStudents).collect(Collectors.toSet()); + } + + /** + * Loads additional details for team exercises and for active quiz exercises + * Assumes that the exercises are already been loaded (i.e. no proxy) + * + * @param course corresponding course: exercises + * @param user the user entity + */ + public void loadExerciseDetailsIfNecessary(Course course, User user) { + for (Exercise exercise : course.getExercises()) { + // only necessary for team exercises + setAssignedTeamIdForExerciseAndUser(exercise, user); - // filter out questions and all statistical information about the quizPointStatistic from quizExercises (so users can't see which answer options are correct) - if (exercise instanceof QuizExercise quizExercise) { - quizExercise.filterSensitiveInformation(); + // filter out questions and all statistical information about the quizPointStatistic from quizExercises (so users can't see which answer options are correct) + if (exercise instanceof QuizExercise quizExercise) { + quizExercise.filterSensitiveInformation(); - // if the quiz is not active the batches do not matter and there is no point in loading them - if (quizExercise.isQuizStarted() && !quizExercise.isQuizEnded()) { - // delete the proxy as it doesn't work; getQuizBatchForStudent will load the batches from the DB directly - quizExercise.setQuizBatches(null); - quizExercise.setQuizBatches(quizBatchService.getQuizBatchForStudentByLogin(quizExercise, user.getLogin()).stream().collect(Collectors.toSet())); - } + // if the quiz is not active the batches do not matter and there is no point in loading them + if (quizExercise.isQuizStarted() && !quizExercise.isQuizEnded()) { + // delete the proxy as it doesn't work; getQuizBatchForStudent will load the batches from the DB directly + quizExercise.setQuizBatches(null); + quizExercise.setQuizBatches(quizBatchService.getQuizBatchForStudentByLogin(quizExercise, user.getLogin()).stream().collect(Collectors.toSet())); } } } - - return exercises; } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizBatchService.java b/src/main/java/de/tum/in/www1/artemis/service/QuizBatchService.java index 198bcc0bcb3c..2b392726ae46 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizBatchService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/QuizBatchService.java @@ -167,7 +167,8 @@ public ZonedDateTime quizBatchStartDate(QuizExercise quizExercise, ZonedDateTime } /** - * Return the batch that a user the currently participating in for a given exercise + * Return the batch that a user the currently participating in for a given quiz exercise + * Note: This method will definitely include a database read query * @param quizExercise the quiz for that the batch should be look up for * @param login the login of the user that the batch should be looked up for * @return the batch that the user currently takes part in or empty diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizScheduleService.java index 2b70216b1f48..828260ebb38c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizScheduleService.java @@ -527,7 +527,8 @@ public void joinQuizBatch(QuizExercise quizExercise, QuizBatch quizBatch, User u } public Optional getQuizBatchForStudentByLogin(QuizExercise quizExercise, String login) { - return Optional.ofNullable(((QuizExerciseCache)quizCache.getReadCacheFor(quizExercise.getId())).getBatches().get(login)); + var quizExerciseCache = (QuizExerciseCache)quizCache.getReadCacheFor(quizExercise.getId()); + return Optional.ofNullable(quizExerciseCache.getBatches().get(login)); } private void removeCachedQuiz(QuizExerciseCache cachedQuiz) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index 5760be0c1fcb..d52c98a0269a 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -34,6 +34,7 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; import de.tum.in.www1.artemis.domain.participation.TutorParticipation; import de.tum.in.www1.artemis.exception.ArtemisAuthenticationException; import de.tum.in.www1.artemis.repository.*; @@ -48,6 +49,7 @@ import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggle; import de.tum.in.www1.artemis.service.tutorialgroups.TutorialGroupsConfigurationService; +import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.dto.*; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @@ -406,37 +408,49 @@ public List getAllCoursesToRegister() { * GET /courses/{courseId}/for-dashboard * * @param courseId the courseId for which exercises, lectures, exams and learning goals should be fetched - * @return a course with all exercises, lectures, exams and learning goals visible to the student + * @return a course with all exercises, lectures, exams, learning goals, etc. visible to the user */ + // TODO: we should rename this into courses/{courseId}/details @GetMapping("courses/{courseId}/for-dashboard") @PreAuthorize("hasRole('USER')") public Course getCourseForDashboard(@PathVariable long courseId) { - long start = System.currentTimeMillis(); + long timeNanoStart = System.nanoTime(); + log.debug("REST request to get one course {} with exams, lectures, exercises, participations, submissions and results, etc.", courseId); User user = userRepository.getUserWithGroupsAndAuthorities(); - Course course = courseService.findOneWithExercisesAndLecturesAndExamsAndLearningGoalsAndTutorialGroupsForUser(courseId, user); - courseService.fetchParticipationsWithSubmissionsAndResultsForCourses(List.of(course), user, start); + courseService.fetchParticipationsWithSubmissionsAndResultsForCourses(List.of(course), user); + logDuration(List.of(course), user, timeNanoStart); return course; } /** * GET /courses/for-dashboard * - * @return the list of courses (the user has access to) including all exercises with participation and result for the user + * @return the list of courses (the user has access to) including all exercises with participation, submission and result, etc. for the user */ @GetMapping("courses/for-dashboard") @PreAuthorize("hasRole('USER')") public List getAllCoursesForDashboard() { - long start = System.currentTimeMillis(); - log.debug("REST request to get all Courses the user has access to with exercises, participations and results"); + long timeNanoStart = System.nanoTime(); User user = userRepository.getUserWithGroupsAndAuthorities(); - - // get all courses with exercises for this user + log.debug("REST request to get all courses the user {} has access to with exams, lectures, exercises, participations, submissions and results", user.getLogin()); List courses = courseService.findAllActiveWithExercisesAndLecturesAndExamsForUser(user); - courseService.fetchParticipationsWithSubmissionsAndResultsForCourses(courses, user, start); + courseService.fetchParticipationsWithSubmissionsAndResultsForCourses(courses, user); + logDuration(courses, user, timeNanoStart); return courses; } + private void logDuration(List courses, User user, long timeNanoStart) { + if (log.isInfoEnabled()) { + Set exercises = courses.stream().flatMap(course -> course.getExercises().stream()).collect(Collectors.toSet()); + Map> exercisesGroupedByExerciseMode = exercises.stream().collect(Collectors.groupingBy(Exercise::getMode)); + int noOfIndividualExercises = Objects.requireNonNullElse(exercisesGroupedByExerciseMode.get(ExerciseMode.INDIVIDUAL), List.of()).size(); + int noOfTeamExercises = Objects.requireNonNullElse(exercisesGroupedByExerciseMode.get(ExerciseMode.TEAM), List.of()).size(); + log.info("/courses/for-dashboard finished in {} for {} courses with {} individual exercises and {} team exercises for user {}", + TimeLogUtil.formatDurationFrom(timeNanoStart), courses.size(), noOfIndividualExercises, noOfTeamExercises, user.getLogin()); + } + } + /** * GET /courses/for-notifications * diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java index b312507983e4..2a714313c442 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java @@ -439,7 +439,7 @@ public ResponseEntity performActionForQuizExercise(@PathVariable L .headers(HeaderUtil.createFailureAlert(applicationName, true, "quizExercise", "quizAlreadyStarted", "Quiz has already started.")).build(); } - // set release date to now, truncated to seconds because the database only stores seconds + // set release date to now, truncated to seconds var now = ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS); quizBatchService.getOrCreateSynchronizedQuizBatch(quizExercise).setStartTime(now); if (quizExercise.getReleaseDate() != null && quizExercise.getReleaseDate().isAfter(now)) { diff --git a/src/main/webapp/app/course/manage/course-management.component.html b/src/main/webapp/app/course/manage/course-management.component.html index 6ed19ddf09f2..0c6ffe029520 100644 --- a/src/main/webapp/app/course/manage/course-management.component.html +++ b/src/main/webapp/app/course/manage/course-management.component.html @@ -25,16 +25,14 @@

-
- - - {{ 'artemisApp.course.semester' | artemisTranslate }}: {{ semester }} +
+ + {{ 'artemisApp.course.semester' | artemisTranslate }}: {{ semester }} + + {{ 'artemisApp.course.semester' | artemisTranslate }}: {{ 'global.generic.unset' | artemisTranslate }} - - {{ 'artemisApp.course.semester' | artemisTranslate }}: {{ 'global.generic.unset' | artemisTranslate }} - - - {{ 'artemisApp.course.testCourse.plural' | artemisTranslate }} + + {{ 'artemisApp.course.testCourse.plural' | artemisTranslate }}
diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.component.html b/src/main/webapp/app/course/manage/overview/course-management-card.component.html index 11d60a72dbab..5e6232625d4d 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.component.html +++ b/src/main/webapp/app/course/manage/overview/course-management-card.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.component.ts b/src/main/webapp/app/course/manage/overview/course-management-card.component.ts index 71cbf20a0af0..605ba3383a2e 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.component.ts +++ b/src/main/webapp/app/course/manage/overview/course-management-card.component.ts @@ -33,6 +33,7 @@ export class CourseManagementCardComponent implements OnChanges { readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; CachingStrategy = CachingStrategy; + // TODO: can we merge the 3 courses here? @Input() course: Course; @Input() courseStatistics: CourseManagementOverviewStatisticsDto; @Input() courseWithExercises: Course; diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.scss b/src/main/webapp/app/course/manage/overview/course-management-card.scss index 1c3661c7317b..b378a1855fea 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.scss +++ b/src/main/webapp/app/course/manage/overview/course-management-card.scss @@ -160,7 +160,7 @@ } .no-exercises { - font-weight: 300; + font-weight: 400; } } diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.scss b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.scss index 8d148263f9e0..48de90a9d7fe 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.scss +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.scss @@ -6,7 +6,7 @@ gap: 10px; font-size: 1.3rem; line-height: 1.2; - font-weight: 300; + font-weight: 400; .right-col { flex: 0 0 auto; diff --git a/src/main/webapp/app/exercises/shared/exercise-hint/participate/exercise-hint-expandable.component.scss b/src/main/webapp/app/exercises/shared/exercise-hint/participate/exercise-hint-expandable.component.scss index 80cca5585b4d..0768618c0d5d 100644 --- a/src/main/webapp/app/exercises/shared/exercise-hint/participate/exercise-hint-expandable.component.scss +++ b/src/main/webapp/app/exercises/shared/exercise-hint/participate/exercise-hint-expandable.component.scss @@ -14,7 +14,7 @@ .task-name { font-size: 12px; - font-weight: lighter; + font-weight: 300; } .mat-expansion-panel-body { diff --git a/src/main/webapp/app/overview/course-exercises/course-exercises.component.html b/src/main/webapp/app/overview/course-exercises/course-exercises.component.html index 92553b4993ea..38bb76ad17f5 100644 --- a/src/main/webapp/app/overview/course-exercises/course-exercises.component.html +++ b/src/main/webapp/app/overview/course-exercises/course-exercises.component.html @@ -125,18 +125,19 @@

> - {{ weeklyExercisesGrouped[weekKey].start | artemisDate : 'long-date' }} - - {{ weeklyExercisesGrouped[weekKey].end | artemisDate : 'long-date' }} + {{ weeklyExercisesGrouped[weekKey].start | artemisDate : 'long-date' }} - + {{ weeklyExercisesGrouped[weekKey].end | artemisDate : 'long-date' }} {{ 'artemisApp.courseOverview.exerciseList.noDateAssociated' | artemisTranslate }} - Exercises: {{ weeklyExercisesGrouped[weekKey].exercises.length }} + (Exercises: {{ weeklyExercisesGrouped[weekKey].exercises.length }})

diff --git a/src/main/webapp/app/overview/course-lectures/course-lectures.component.html b/src/main/webapp/app/overview/course-lectures/course-lectures.component.html index ad006d7bf245..b769e206d88e 100644 --- a/src/main/webapp/app/overview/course-lectures/course-lectures.component.html +++ b/src/main/webapp/app/overview/course-lectures/course-lectures.component.html @@ -29,14 +29,15 @@ > - {{ weeklyLecturesGrouped[weekKey].start | artemisDate : 'long-date' }} - - {{ weeklyLecturesGrouped[weekKey].end | artemisDate : 'long-date' }} + {{ weeklyLecturesGrouped[weekKey].start | artemisDate : 'long-date' }} - + {{ weeklyLecturesGrouped[weekKey].end | artemisDate : 'long-date' }} {{ 'artemisApp.courseOverview.exerciseList.noDateAssociated' | artemisTranslate }} diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index fd087185d435..f5b8c39c8483 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -13,13 +13,13 @@ routerLink="exams" routerLinkActive="active" > - Exams +
Exams
- Exercise +
Exercise
- Lectures +
Lectures
- Learning Goals +
Learning Goals
- Statistics +
Statistics
- Communication +
Communication
- Messages +
Messages
- Tutorial Groups +
Tutorial Groups
diff --git a/src/main/webapp/app/shared/circular-progress-bar/circular-progress-bar.component.scss b/src/main/webapp/app/shared/circular-progress-bar/circular-progress-bar.component.scss index 0cfcb983ce94..2538bafcac0e 100644 --- a/src/main/webapp/app/shared/circular-progress-bar/circular-progress-bar.component.scss +++ b/src/main/webapp/app/shared/circular-progress-bar/circular-progress-bar.component.scss @@ -92,7 +92,7 @@ $howManySteps: 100; //this needs to be even. justify-content: center; height: 100%; //font-family: $work-sans; - font-weight: 300; + font-weight: 400; div { margin-top: 10px; diff --git a/src/main/webapp/app/shared/layouts/main/main.component.html b/src/main/webapp/app/shared/layouts/main/main.component.html index db7aa1667596..4a9062b0d86f 100644 --- a/src/main/webapp/app/shared/layouts/main/main.component.html +++ b/src/main/webapp/app/shared/layouts/main/main.component.html @@ -4,11 +4,9 @@
-
-
-
- -
+
+
+
diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.scss b/src/main/webapp/app/shared/layouts/navbar/navbar.scss index 0b64c70393fd..b6d0751e305f 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.scss +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.scss @@ -202,10 +202,10 @@ a:not(.btn):hover { background-color: var(--navbar-breadcrumb-background); margin-left: 1rem; margin-bottom: 0; - padding: 10px 16px 10px 16px; + padding: 5px 16px 5px 16px; .breadcrumb-link { - font-weight: lighter; + font-weight: 300; display: inline; } } diff --git a/src/main/webapp/content/scss/global.scss b/src/main/webapp/content/scss/global.scss index c26c6cc3f001..ffc0beee42b2 100644 --- a/src/main/webapp/content/scss/global.scss +++ b/src/main/webapp/content/scss/global.scss @@ -25,7 +25,7 @@ h1, h2, h3, h4 { - font-weight: 300; + font-weight: 400; } /* like fw-bold or fw-normal, but with weight 500 */ @@ -486,6 +486,10 @@ span.bold { font-weight: bold; } +bold { + font-weight: 500; +} + .negative, .positive { padding: 2px 4px; diff --git a/src/main/webapp/content/scss/themes/_dark-variables.scss b/src/main/webapp/content/scss/themes/_dark-variables.scss index ccb76092eb64..4ddc73773ed7 100644 --- a/src/main/webapp/content/scss/themes/_dark-variables.scss +++ b/src/main/webapp/content/scss/themes/_dark-variables.scss @@ -37,7 +37,7 @@ $min-contrast-ratio: 1.9; $body-color: $gray-100; $text-muted: $gray-600; -$body-bg: $gray-900; +$body-bg: $gray-800; $input-color: $gray-800; $input-border-color: $body-bg; diff --git a/src/main/webapp/content/scss/themes/_default-variables.scss b/src/main/webapp/content/scss/themes/_default-variables.scss index 5e9a9f92ab91..25046977760e 100644 --- a/src/main/webapp/content/scss/themes/_default-variables.scss +++ b/src/main/webapp/content/scss/themes/_default-variables.scss @@ -35,7 +35,7 @@ $dark: $gray-900; $body-color: $gray-900; $text-muted: $gray-600; -$body-bg: $gray-100; +$body-bg: $gray-200; $navbar-dark-color: rgba($white, 0.6); $navbar-dark-hover-color: $white; @@ -54,7 +54,7 @@ $footer-bg: #fff; $navbar-foreground: #ccc; $navbar-bg: $artemis-dark; $navbar-item-active-hover-bg: $artemis-dark; -$navbar-breadcrumb-background: #e4e5e6; +$navbar-breadcrumb-background: $body-bg; $navbar-breadcrumb-color: #0065bd; // === END BOOTSTRAP === diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 9c7384537e6a..918be2e1dd29 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -46,7 +46,7 @@ "overdue": "Keine Überfälligen", "optional": "Keine Optionalen", "noExerciseDate": "Kein Datum zugeordnet", - "exerciseGroupHeader": "Aufgaben: {{ total }}", + "exerciseGroupHeader": "(Aufgaben: {{ total }})", "currentExerciseGroupHeader": "Aktuelle Übung aktiv bis {{ date }}:", "currentExerciseGroupHeaderWithoutDueDate": "Aktuelle Übung:", "sortExercises": "Aufgaben sortieren", @@ -140,7 +140,7 @@ "noData": "Es sind keine Lernziele für diesen Kurs vorhanden." }, "lectureList": { - "lectureGroupHeader": "Vorlesungen: {{ total }}", + "lectureGroupHeader": "(Vorlesungen: {{ total }})", "totalLectures": "Vorlesungen insgesamt:", "totalAttachments": "Anhänge insgesamt:", "isLecture": "Das ist eine Vorlesung", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 13f3d666505a..c00f6d14fae7 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -46,7 +46,7 @@ "overdue": "Hide overdue", "optional": "Hide optional", "noExerciseDate": "No date associated", - "exerciseGroupHeader": "Exercises: {{ total }}", + "exerciseGroupHeader": "(Exercises: {{ total }})", "currentExerciseGroupHeader": "Current exercise active until {{ date }}:", "currentExerciseGroupHeaderWithoutDueDate": "Current exercise:", "sortExercises": "Sort exercises", @@ -142,7 +142,7 @@ "noData": "No learning goals available for this course." }, "lectureList": { - "lectureGroupHeader": "Lectures: {{ total }}", + "lectureGroupHeader": "(Lectures: {{ total }})", "totalLectures": "Total lectures:", "totalAttachments": "Total attachments:", "isLecture": "This is a lecture", diff --git a/src/test/java/de/tum/in/www1/artemis/DatabaseQueryCountTest.java b/src/test/java/de/tum/in/www1/artemis/DatabaseQueryCountTest.java index 479758dcb4de..624c19ef5e5f 100644 --- a/src/test/java/de/tum/in/www1/artemis/DatabaseQueryCountTest.java +++ b/src/test/java/de/tum/in/www1/artemis/DatabaseQueryCountTest.java @@ -27,14 +27,38 @@ void testGetAllCoursesForDashboardRealisticQueryCount() throws Exception { String suffix = "cfdr"; database.adjustUserGroupsToCustomGroups(TEST_PREFIX, suffix, 1, 5, 0, 0); // Tests the amount of DB calls for a 'realistic' call to courses/for-dashboard. We should aim to maintain or lower the amount of DB calls, and be aware if they increase + // TODO: add team exercises, do not make all quizzes active var courses = database.createMultipleCoursesWithAllExercisesAndLectures(TEST_PREFIX, 10, 10); database.updateCourseGroups(TEST_PREFIX, courses, suffix); assertThatDb(() -> { - log.info("Start courses for dashboard call"); + log.info("Start courses for dashboard call for multiple courses"); var userCourses = request.getList("/api/courses/for-dashboard", HttpStatus.OK, Course.class); - log.info("Finish courses for dashboard call"); + log.info("Finish courses for dashboard call for multiple courses"); return userCourses; - }).hasBeenCalledAtMostTimes(34); + }).hasBeenCalledAtMostTimes(15); + // 1 DB call to get the user from the DB + // 1 DB call to get the course with exercise, lectures, exams + // 1 DB call to load all exercises + // 10 DB calls to get the quiz batches for active quiz exercises + // 1 DB call to get all individual student participations with submissions and results + // 1 DB call to get all team student participations with submissions and results + + var course = courses.get(0); + assertThatDb(() -> { + log.info("Start course for dashboard call for one course"); + var userCourse = request.get("/api/courses/" + course.getId() + "/for-dashboard", HttpStatus.OK, Course.class); + log.info("Finish courses for dashboard call for one course"); + return userCourse; + }).hasBeenCalledAtMostTimes(9); + // 1 DB call to get the user from the DB + // 1 DB call to get the course with exercise, lectures, exams + // 1 DB call to load all exercises + // 1 DB call to load all learning goals + // 1 DB call to load all prerequisite + // 1 DB call to load all tutorial groups + // 1 DB call to load the tutorial group configuration + // 1 DB call to get all individual student participations with submissions and results + // 1 DB call to get all team student participations with submissions and results } }