Skip to content

Commit

Permalink
Merge branch 'develop' into feature/general/delete-not-enrolled-users
Browse files Browse the repository at this point in the history
  • Loading branch information
BaumiCoder authored Sep 6, 2024
2 parents 0ee60cc + 1ac30d7 commit a28e692
Show file tree
Hide file tree
Showing 81 changed files with 2,839 additions and 1,504 deletions.
3 changes: 3 additions & 0 deletions docker/artemis/config/playwright.env
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ ARTEMIS_CONTINUOUSINTEGRATION_EMPTYCOMMITNECESSARY="true"

ARTEMIS_APOLLON_CONVERSIONSERVICEURL="https://apollon.ase.in.tum.de/api/converter"

ARTEMIS_TELEMETRY_ENABLED="false"

# Token is valid 3 days
JHIPSTER_SECURITY_AUTHENTICATION_JWT_TOKENVALIDITYINSECONDS="259200"
# Token is valid 30 days
Expand All @@ -38,6 +40,7 @@ INFO_IMPRINT="https://ase.in.tum.de/lehrstuhl_1/component/content/article/179-im
INFO_TESTSERVER="true"
INFO_TEXTASSESSMENTANALYTICSENABLED="true"
INFO_STUDENTEXAMSTORESESSIONDATA="true"
INFO_OPERATORNAME="TUM"

LOGGING_FILE_NAME="/opt/artemis/data/artemis.log"

Expand Down
1 change: 1 addition & 0 deletions docs/admin/setup/customization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ instead of the TUM defaults:
* The logo next to the “Artemis” heading on the navbar → ``${artemisRunDirectory}/public/images/logo.png``
* The favicon → ``${artemisRunDirectory}/logo/favicon.svg``
* The contact email address in the ``application-{dev,prod}.yml`` configuration file under the key ``info.contact``
* The operator's name (e.g. university) and the operator's contact information (administrator email address and name) can be specified in the ``application-{dev,prod}.yml`` configuration file under the keys ``info.operatorName``, ``info.contact`` and ``info.operatorAdminName``. These values are also displayed on the ``/about`` page. The operator's name is required, while the administrator's name is optional. Artemis also uses this information for the :ref:`telemetry` service.
* The maximal number of plagiarism results stored per plagiarism checks in the ``application-{dev,prod}.yml`` configuration file under the key ``artemis.plagiarism-checks.plagiarism-results-limit``
37 changes: 37 additions & 0 deletions docs/admin/telemetry.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.. _telemetry:

Telemetry
=========

To help to improve Artemis, we collect some data when the application starts.
This feature can be disabled by setting `telemetry.enabled` in the `application-prod.yml` to `false`.
When this is set to false, no data is sent to the Artemis maintainer team.
By setting `telemetry.sendAdminDetails` to false, personal information of the instance's admin (i.e. contact email and name) is excluded from the telemetry data.
This includes the contact email and the administrator's name.

Artemis collects the following data at the startup of an instance:

* The used Artemis version
* The contact email address of the admin, which is set in `info.contact`
* The name of the admin, set in `info.operatorAdminName` (optional)
* The server's URL
* The operator's name
* The used profiles (e.g. Gitlab, Jenkins, LocalVC, Aeolus, ...)

Example configuration in `application-prod.yml`:

.. code-block:: yaml
artemis:
telemetry:
enabled: true
sendAdminDetails: false
destination: telemetry.artemis.cit.tum.de
info:
contact: [email protected]
operatorName: Technical University of Munich
operatorAdminName: Stephan Krusche
We collect this data to enhance Artemis by understanding how it is used, ensuring compatibility with different configurations, and providing better support to our users.
Collecting admin contact information allows us to communicate important updates or address critical issues directly.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ All these exercises are supposed to be run either live in the lecture with insta
admin/database
admin/knownIssues
admin/benchmarking-tool
admin/telemetry


.. toctree::
Expand Down
1,230 changes: 859 additions & 371 deletions package-lock.json

Large diffs are not rendered by default.

59 changes: 31 additions & 28 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@
"node_modules"
],
"dependencies": {
"@angular/animations": "18.2.2",
"@angular/cdk": "18.2.2",
"@angular/common": "18.2.2",
"@angular/compiler": "18.2.2",
"@angular/core": "18.2.2",
"@angular/forms": "18.2.2",
"@angular/localize": "18.2.2",
"@angular/material": "18.2.2",
"@angular/platform-browser": "18.2.2",
"@angular/platform-browser-dynamic": "18.2.2",
"@angular/router": "18.2.2",
"@angular/service-worker": "18.2.2",
"@angular/animations": "18.2.3",
"@angular/cdk": "18.2.3",
"@angular/common": "18.2.3",
"@angular/compiler": "18.2.3",
"@angular/core": "18.2.3",
"@angular/forms": "18.2.3",
"@angular/localize": "18.2.3",
"@angular/material": "18.2.3",
"@angular/platform-browser": "18.2.3",
"@angular/platform-browser-dynamic": "18.2.3",
"@angular/router": "18.2.3",
"@angular/service-worker": "18.2.3",
"@ctrl/ngx-emoji-mart": "9.2.0",
"@danielmoncada/angular-datetime-picker": "18.1.0",
"@fingerprintjs/fingerprintjs": "4.4.3",
Expand All @@ -37,7 +37,7 @@
"@ng-bootstrap/ng-bootstrap": "17.0.1",
"@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "8.0.0",
"@sentry/angular": "8.27.0",
"@sentry/angular": "8.28.0",
"@swimlane/ngx-charts": "20.5.0",
"@swimlane/ngx-graph": "8.4.0",
"@vscode/codicons": "0.0.36",
Expand All @@ -46,9 +46,9 @@
"core-js": "3.38.1",
"crypto-js": "4.2.0",
"dayjs": "1.11.13",
"diff-match-patch-typescript": "1.0.8",
"diff-match-patch-typescript": "1.1.0",
"dompurify": "3.1.6",
"export-to-csv": "1.3.0",
"export-to-csv": "1.4.0",
"fast-json-patch": "3.1.1",
"franc-min": "6.2.0",
"html-diff-ts": "1.4.2",
Expand All @@ -62,7 +62,8 @@
"ngx-infinite-scroll": "18.0.0",
"ngx-webstorage": "18.0.0",
"papaparse": "5.4.1",
"posthog-js": "1.160.0",
"pdfjs-dist": "4.6.82",
"posthog-js": "1.160.3",
"rxjs": "7.8.1",
"showdown": "2.1.0",
"showdown-highlight": "3.1.0",
Expand Down Expand Up @@ -96,11 +97,12 @@
"eslint": "^9.9.0"
},
"eslint-plugin-jest": {
"@typescript-eslint/eslint-plugin": "^8.1.0"
"@typescript-eslint/eslint-plugin": "^8.4.0"
},
"jsdom": "24.1.1",
"katex": "0.16.11",
"postcss": "8.4.41",
"rimraf": "6.0.1",
"semver": "7.6.3",
"showdown-katex": {
"showdown": "2.1.0"
Expand All @@ -113,33 +115,33 @@
},
"devDependencies": {
"@angular-builders/jest": "18.0.0",
"@angular-devkit/build-angular": "18.2.2",
"@angular-devkit/build-angular": "18.2.3",
"@angular-eslint/builder": "18.3.0",
"@angular-eslint/eslint-plugin": "18.3.0",
"@angular-eslint/eslint-plugin-template": "18.3.0",
"@angular-eslint/schematics": "18.3.0",
"@angular-eslint/template-parser": "18.3.0",
"@angular/cli": "18.2.2",
"@angular/compiler-cli": "18.2.2",
"@angular/language-service": "18.2.2",
"@sentry/types": "8.27.0",
"@angular/cli": "18.2.3",
"@angular/compiler-cli": "18.2.3",
"@angular/language-service": "18.2.3",
"@sentry/types": "8.28.0",
"@types/crypto-js": "4.2.2",
"@types/d3-shape": "3.1.6",
"@types/dompurify": "3.0.5",
"@types/jest": "29.5.12",
"@types/lodash-es": "4.17.12",
"@types/node": "22.5.1",
"@types/node": "22.5.4",
"@types/papaparse": "5.3.14",
"@types/showdown": "2.0.6",
"@types/smoothscroll-polyfill": "0.3.4",
"@types/sockjs-client": "1.5.4",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.3.0",
"@typescript-eslint/parser": "8.3.0",
"@typescript-eslint/eslint-plugin": "8.4.0",
"@typescript-eslint/parser": "8.4.0",
"eslint": "9.9.1",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-deprecation": "3.0.0",
"eslint-plugin-jest": "28.8.1",
"eslint-plugin-jest": "28.8.3",
"eslint-plugin-jest-extended": "2.4.0",
"eslint-plugin-prettier": "5.2.1",
"folder-hash": "4.0.4",
Expand All @@ -151,10 +153,11 @@
"jest-fail-on-console": "3.3.0",
"jest-junit": "16.0.0",
"jest-preset-angular": "14.2.2",
"lint-staged": "15.2.9",
"lint-staged": "15.2.10",
"ng-mocks": "14.13.1",
"prettier": "3.3.3",
"sass": "1.77.8",
"rimraf": "6.0.1",
"sass": "1.78.0",
"ts-jest": "29.2.5",
"typescript": "5.5.4",
"weak-napi": "2.0.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package de.tum.in.www1.artemis.config;

import static de.tum.in.www1.artemis.config.Constants.PROFILE_SCHEDULING;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile(PROFILE_SCHEDULING)
public class PropertiesConfigurationGuard implements InitializingBean {

private static final Logger log = LoggerFactory.getLogger(PropertiesConfigurationGuard.class);

@Value("${info.operatorName:#{null}}")
private String operatorName;

/**
* Checks if the info.operatorName value is set in the configuration ymls, and exits the application if not.
*/
public void afterPropertiesSet() {
if (this.operatorName == null || this.operatorName.isEmpty()) {
log.error(
"The name of the operator (University) is not configured in the application-prod.yml! It is needed to be displayed in the /about page, and for the telemetry service.");
throw new IllegalArgumentException("The name of the operator (university) must be configured, but is not!");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.stereotype.Component;

import de.tum.in.www1.artemis.security.jwt.JWTFilter;
import de.tum.in.www1.artemis.service.OnlineCourseConfigurationService;
import de.tum.in.www1.artemis.service.connectors.lti.Lti13Service;
import de.tum.in.www1.artemis.web.filter.Lti13LaunchFilter;
Expand Down Expand Up @@ -74,7 +74,7 @@ public void configure(HttpSecurity http) {
// https://www.imsglobal.org/spec/security/v1p0/#step-3-authentication-response
OAuth2LoginAuthenticationFilter defaultLoginFilter = configureLoginFilter(clientRegistrationRepository(http), oidcLaunchFlowAuthenticationProvider,
authorizationRequestRepository);
http.addFilterAfter(new Lti13LaunchFilter(defaultLoginFilter, "/" + LTI13_LOGIN_PATH, lti13Service(http)), AbstractPreAuthenticatedProcessingFilter.class);
http.addFilterAfter(new Lti13LaunchFilter(defaultLoginFilter, "/" + LTI13_LOGIN_PATH, lti13Service(http)), JWTFilter.class);
}

protected Lti13Service lti13Service(HttpSecurity http) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -703,18 +703,38 @@ default Page<StudentParticipation> findAllWithEagerSubmissionsAndResultsByExerci
""")
List<StudentParticipation> findAllWithEagerLegalSubmissionsAndEagerResultsByExerciseId(@Param("exerciseId") long exerciseId);

/**
* Retrieves all distinct `StudentParticipation` entities for a specific exercise,
* including their latest non-illegal submission and the latest rated result for each submission.
* The method fetches related submissions, results, student, and team data to avoid the N+1 select problem.
*
* <p>
* The method ensures that:
* <ul>
* <li>Only participations belonging to the specified exercise are retrieved.</li>
* <li>Participations marked as a test run are excluded.</li>
* <li>Only the latest non-illegal submission for each participation is considered.</li>
* <li>Only the latest rated result for each submission is considered.</li>
* </ul>
*
* @param exerciseId the ID of the exercise for which to retrieve participations.
* @return a list of distinct `StudentParticipation` entities matching the criteria.
*/
@Query("""
SELECT DISTINCT p
FROM StudentParticipation p
LEFT JOIN FETCH p.results r
LEFT JOIN FETCH r.submission rs
LEFT JOIN FETCH p.submissions s
LEFT JOIN FETCH s.results sr
LEFT JOIN FETCH s.results r
LEFT JOIN FETCH p.student
LEFT JOIN FETCH p.team
WHERE p.exercise.id = :exerciseId
AND p.testRun = FALSE
AND p.submissions IS NOT EMPTY
AND (s.type <> de.tum.in.www1.artemis.domain.enumeration.SubmissionType.ILLEGAL OR s.type IS NULL)
AND (rs.type <> de.tum.in.www1.artemis.domain.enumeration.SubmissionType.ILLEGAL OR rs.type IS NULL)
AND s.id = (SELECT MAX(s2.id)
FROM p.submissions s2
WHERE s2.type <> de.tum.in.www1.artemis.domain.enumeration.SubmissionType.ILLEGAL OR s2.type IS NULL)
AND r.id = (SELECT MAX(r2.id)
FROM s.results r2
WHERE r2.rated = TRUE)
""")
List<StudentParticipation> findAllForPlagiarism(@Param("exerciseId") long exerciseId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
Expand Down Expand Up @@ -42,6 +43,9 @@
@Profile("lti")
public class LtiService {

@Value("${artemis.lti.trustExternalLTISystems:false}")
private boolean trustExternalLTISystems;

public static final String LTI_GROUP_NAME = "lti";

protected static final List<SimpleGrantedAuthority> SIMPLE_USER_LIST_AUTHORITY = Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority()));
Expand Down Expand Up @@ -105,6 +109,14 @@ public void authenticateLtiUser(String email, String username, String firstName,
// 2. Case: Lookup user with the LTI email address and make sure it's not in use
if (artemisAuthenticationProvider.getUsernameForEmail(email).isPresent() || userRepository.findOneByEmailIgnoreCase(email).isPresent()) {
log.info("User with email {} already exists. Email is already in use.", email);

if (trustExternalLTISystems) {
log.info("Trusting external LTI system. Authenticating user with email: {}", email);
User user = userRepository.findUserWithGroupsAndAuthoritiesByEmail(email).orElseThrow();
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user.getLogin(), user.getPassword(), user.getGrantedAuthorities()));
return;
}

throw new LtiEmailAlreadyInUseException();
}

Expand Down Expand Up @@ -179,23 +191,16 @@ private void addUserToExerciseGroup(User user, Course course) {
* @param response the response to add the JWT cookie to
*/
public void buildLtiResponse(UriComponentsBuilder uriComponentsBuilder, HttpServletResponse response) {
// TODO SK: why do we logout the user here if it was already activated?

User user = userRepository.getUser();

if (!user.getActivated()) {
log.info("User is not activated. Adding JWT cookie for activation.");
log.info("Add JWT cookie so the user will be logged in");
ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(true);
response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());

log.info("User is not activated. Adding initialize parameter to query.");
uriComponentsBuilder.queryParam("initialize", "");
}
else {
log.info("User is activated. Adding JWT cookie for logout.");
prepareLogoutCookie(response);
uriComponentsBuilder.queryParam("ltiSuccessLoginRequired", user.getLogin());
}

log.info("Add/Update JWT cookie so the user will be logged in.");
ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(true);
response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float sim
private JPlagResult computeJPlagResult(ProgrammingExercise programmingExercise, float similarityThreshold, int minimumScore, int minimumSize) {
long programmingExerciseId = programmingExercise.getId();
final var targetPath = fileService.getTemporaryUniqueSubfolderPath(repoDownloadClonePath, 60);
List<ProgrammingExerciseParticipation> participations = filterStudentParticipationsForComparison(programmingExercise, minimumScore);
List<ProgrammingExerciseParticipation> participations = findStudentParticipationsForComparison(programmingExercise, minimumScore);
log.info("Download repositories for JPlag for programming exercise {} to compare {} participations", programmingExerciseId, participations.size());

if (participations.size() < 2) {
Expand Down Expand Up @@ -326,8 +326,10 @@ private Language getJPlagProgrammingLanguage(ProgrammingExercise programmingExer
* @param minimumScore consider only submissions whose score is greater or equal to this value
* @return an unmodifiable list containing the latest text submission for every participation
*/
public List<ProgrammingExerciseParticipation> filterStudentParticipationsForComparison(ProgrammingExercise programmingExercise, int minimumScore) {
public List<ProgrammingExerciseParticipation> findStudentParticipationsForComparison(ProgrammingExercise programmingExercise, int minimumScore) {
long start = System.nanoTime();
var studentParticipations = studentParticipationRepository.findAllForPlagiarism(programmingExercise.getId());
log.info("findAllForPlagiarism took {}", TimeLogUtil.formatDurationFrom(start));

return studentParticipations.parallelStream().filter(participation -> !participation.isPracticeMode())
.filter(participation -> participation instanceof ProgrammingExerciseStudentParticipation).filter(plagiarismService.filterForStudents())
Expand Down
Loading

0 comments on commit a28e692

Please sign in to comment.