Skip to content
GitLab
    • Explore Projects Groups Snippets
Projects Groups Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
  • Sign in
  • M moodle-assignsubmission_dta
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
    • Locked Files
  • Issues 3
    • Issues 3
    • List
    • Boards
    • Service Desk
    • Milestones
    • Iterations
    • Requirements
  • Merge requests 0
    • Merge requests 0
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
    • Test Cases
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Infrastructure Registry
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Code review
    • Insights
    • Issue
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • CoTA
  • moodle-assignsubmission_dta
  • Merge requests
  • !1

Coding style and recommendations

  • Review changes

  • Download
  • Email patches
  • Plain diff
Merged mamunozgil requested to merge niklas-dev-recommendation into master 4 months ago
  • Overview 0
  • Commits 72
  • Pipelines 2
  • Changes 16

The branch includes Niklas changes to show the correct recommendations after an assignment submission and changes the code style and naming to the moodle standard requirements.

  • mamunozgil @miguel.munoz-gil requested review from @32khsa1mst 4 months ago

    requested review from @32khsa1mst

  • mamunozgil @miguel.munoz-gil assigned to @miguel.munoz-gil 4 months ago

    assigned to @miguel.munoz-gil

  • mamunozgil @miguel.munoz-gil requested review from @01kuni1bif 4 months ago

    requested review from @01kuni1bif

  • Kurzenberger @01kuni1bif added 5 commits 4 months ago

    added 5 commits

    • cee505a2...07cd8123 - 4 commits from branch master
    • 1fb6dbf6 - Merge branch 'master' into niklas-dev-recommendation

    Compare with previous version

  • mamunozgil @miguel.munoz-gil enabled an automatic merge when the pipeline for 1fb6dbf6 succeeds 4 months ago

    enabled an automatic merge when the pipeline for 1fb6dbf6 succeeds

  • mamunozgil @miguel.munoz-gil mentioned in commit 98ce3595 4 months ago

    mentioned in commit 98ce3595

  • mamunozgil @miguel.munoz-gil merged 4 months ago

    merged

  • Loading
  • Loading
  • Loading
  • Loading
  • Loading
  • Loading
  • You're only seeing other activity in the feed. To add a comment, switch to one of the following options.
Please register or sign in to reply
Compare
  • version 1
    cee505a2
    4 months ago

  • master (base)

and
  • latest version
    1fb6dbf6
    72 commits, 4 months ago

  • version 1
    cee505a2
    71 commits, 4 months ago

16 files
+ 1850
- 160

    Preferences

    File browser
    Compare changes
d‎ta‎
cla‎sses‎
mod‎els‎
dta_recomme‎ndation.php‎ +89 -0
dta_res‎ult.php‎ +112 -0
dta_result_‎summary.php‎ +191 -0
pri‎vacy‎
provid‎er.php‎ +25 -1
dta_backen‎d_utils.php‎ +167 -0
dta_db_u‎tils.php‎ +325 -0
dta_view_submi‎ssion_utils.php‎ +646 -0
d‎b‎
insta‎ll.xml‎ +17 -0
lan‎g/en‎
assignsubmis‎sion_dta.php‎ +25 -16
READ‎ME.md‎ +3 -3
locall‎ib.php‎ +176 -133
.gitlab‎-ci.yml‎ +13 -0
READ‎ME.md‎ +13 -7
docker-co‎mpose.yaml‎ +47 -0
dta‎.zip‎ +0 -0
teacher‎-dta.txt‎ +1 -0
dta/classes/models/dta_recommendation.php 0 → 100644
+ 89
- 0
  • View file @ 1fb6dbf6

  • Edit in single-file editor

  • Open in Web IDE

<?php
// This file is part of Moodle - http://moodle.org/.
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Entity class for DTA submission plugin recommendation.
*
* @package assignsubmission_dta
* @copyright 2023 Gero Lueckemeyer
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace assignsubmission_dta\models;
/**
* Entity class for DTA submission plugin recommendation.
*
* @package assignsubmission_dta
* @copyright 2023
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dta_recommendation {
/**
* @var string $topic Topic of the recommendation.
*/
public $topic;
/**
* @var string $exercisename Name of the exercise.
*/
public $exercisename;
/**
* @var string $url URL of the exercise.
*/
public $url;
/**
* @var int $difficulty Difficulty level of the exercise.
*/
public $difficulty;
/**
* @var int $score Score associated with the recommendation.
*/
public $score;
/**
* Decodes the JSON recommendations returned by the backend service call into an array of dta_recommendation objects.
*
* @param string $jsonstring JSON string containing recommendations.
* @return array Array of dta_recommendation objects.
*/
public static function assignsubmission_dta_decode_json_recommendations(string $jsonstring): array {
$response = json_decode($jsonstring);
$recommendations = [];
// Check if recommendations exist.
if (!empty($response->recommendations)) {
foreach ($response->recommendations as $recommendation) {
$rec = new dta_recommendation();
$rec->topic = $recommendation->topic ?? null;
// Map correct fields to the renamed variable names.
$rec->exercisename = $recommendation->url ?? null;
$rec->url = $recommendation->exerciseName ?? null;
$rec->difficulty = $recommendation->difficulty ?? null;
$rec->score = $recommendation->score ?? null;
$recommendations[] = $rec;
}
}
return $recommendations;
}
}
dta/classes/models/dta_result.php 0 → 100644
+ 112
- 0
  • View file @ 1fb6dbf6

  • Edit in single-file editor

  • Open in Web IDE

<?php
// This file is part of Moodle - http://moodle.org/.
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Entity class for DTA submission plugin result.
*
* @package assignsubmission_dta
* @copyright 2023 Gero Lueckemeyer and student project teams
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace assignsubmission_dta\models;
/**
* Entity class for DTA submission plugin result.
*
* @package assignsubmission_dta
* @copyright 2023 Gero Lueckemeyer and student project teams
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dta_result {
/**
* Broadly used in logic, parametrized for easier change.
*/
public const ASSIGNSUBMISSION_DTA_COMPONENT_NAME = 'assignsubmission_dta';
/**
* @var string $packagename Package name of the test.
*/
public $packagename;
/**
* @var string $classname Unit name of the test.
*/
public $classname;
/**
* @var string $name Name of the test.
*/
public $name;
/**
* @var int $state State is defined as:
* 0 UNKNOWN
* 1 SUCCESS
* 2 FAILURE
* 3 COMPILATIONERROR
*/
public $state;
/**
* @var string $failuretype Type of test failure if applicable, empty string otherwise.
*/
public $failuretype;
/**
* @var string $failurereason Reason of test failure if applicable, empty string otherwise.
*/
public $failurereason;
/**
* @var string $stacktrace Stack trace of test failure if applicable, empty string otherwise.
*/
public $stacktrace;
/**
* @var int|string $columnnumber Column number of compile failure if applicable, empty string otherwise.
*/
public $columnnumber;
/**
* @var int|string $linenumber Line number of compile failure if applicable, empty string otherwise.
*/
public $linenumber;
/**
* @var int|string $position Position of compile failure if applicable, empty string otherwise.
*/
public $position;
/**
* Returns the name of a state with the given number for display.
*
* @param int $state Number of the state.
* @return string Name of state as defined.
*/
public static function assignsubmission_dta_get_statename(int $state): string {
if ($state === 1) {
return get_string('tests_successful', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME);
} else if ($state === 2) {
return get_string('failures', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME);
} else if ($state === 3) {
return get_string('compilation_errors', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME);
} else {
return get_string('unknown_state', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME);
}
}
}
dta/classes/models/dta_result_summary.php 0 → 100644
+ 191
- 0
  • View file @ 1fb6dbf6

  • Edit in single-file editor

  • Open in Web IDE

<?php
// This file is part of Moodle - http://moodle.org/.
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains the DTA submission plugin result summary entity class.
*
* @package assignsubmission_dta
* @copyright 2023 Your Name
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace assignsubmission_dta\models;
/**
* Entity class for DTA submission plugin result summary.
*
* This class holds:
* - A timestamp for when the summary was generated.
* - An optional global stack trace (in case the entire process failed).
* - A competency profile of how many tests passed for each competency.
* - A competency profile of the total coverage for each competency.
* - An array of dta_result objects that detail individual test results.
*
* @package assignsubmission_dta
*/
class dta_result_summary {
/** @var int Unix timestamp for the summary. */
public $timestamp;
/** @var string A global stacktrace if the entire run had a fatal error (optional). */
public $globalstacktrace;
/** @var string Semi-colon-separated numbers for competencies actually passed. */
public $successfultestcompetencies;
/** @var string Semi-colon-separated numbers for total tested competencies. */
public $overalltestcompetencies;
/** @var dta_result[] Array of individual test results. */
public $results;
/**
* Decodes a JSON string into a dta_result_summary object.
*
* @param string $jsonstring JSON that includes timestamp, globalstacktrace, competency profiles, and results.
* @return dta_result_summary
*/
public static function assignsubmission_dta_decode_json(string $jsonstring): dta_result_summary {
$response = json_decode($jsonstring);
$summary = new dta_result_summary();
$summary->timestamp = $response->timestamp ?? 0;
$summary->globalstacktrace = $response->globalstacktrace ?? '';
// If your JSON keys are 'successfulTestCompetencyProfile' and 'overallTestCompetencyProfile'.
$summary->successfultestcompetencies = $response->successfulTestCompetencyProfile ?? '';
$summary->overalltestcompetencies = $response->overallTestCompetencyProfile ?? '';
// Decode the "results" array into an array of dta_result objects.
if (!empty($response->results) && is_array($response->results)) {
$summary->results = self::assignsubmission_dta_decode_json_result_array($response->results);
} else {
$summary->results = [];
}
return $summary;
}
/**
* Helper that transforms a list of JSON objects into an array of dta_result objects.
*
* @param array $jsonarray Array of JSON-decoded result objects.
* @return dta_result[]
*/
private static function assignsubmission_dta_decode_json_result_array(array $jsonarray): array {
$ret = [];
foreach ($jsonarray as $entry) {
$value = new dta_result();
$value->packagename = $entry->packageName ?? '';
$value->classname = $entry->className ?? '';
$value->name = $entry->name ?? '';
$value->state = $entry->state ?? 0;
$value->failuretype = $entry->failureType ?? '';
$value->failurereason = $entry->failureReason ?? '';
$value->stacktrace = $entry->stacktrace ?? '';
$value->columnnumber = $entry->columnNumber ?? 0;
$value->linenumber = $entry->lineNumber ?? 0;
$value->position = $entry->position ?? 0;
$ret[] = $value;
}
return $ret;
}
/**
* Get the total number of results (tests) recorded in this summary.
*
* @return int
*/
public function assignsubmission_dta_result_count(): int {
return count($this->results);
}
/**
* Generic helper to count how many results have the given $state.
*
* States can be:
* 0 => unknown
* 1 => success
* 2 => fail
* 3 => compilation error
*
* @param int $state The numeric state code to match.
* @return int Number of results with that state.
*/
public function assignsubmission_dta_state_occurence_count(int $state): int {
$num = 0;
foreach ($this->results as $r) {
if ((int)$r->state === $state) {
$num++;
}
}
return $num;
}
/**
* Count how many results had compilation errors (state=3).
*
* @return int
*/
public function assignsubmission_dta_compilation_error_count(): int {
return $this->assignsubmission_dta_state_occurence_count(3);
}
/**
* Count how many results failed (state=2).
*
* @return int
*/
public function assignsubmission_dta_failed_count(): int {
return $this->assignsubmission_dta_state_occurence_count(2);
}
/**
* Count how many results were successful (state=1).
*
* @return int
*/
public function assignsubmission_dta_successful_count(): int {
return $this->assignsubmission_dta_state_occurence_count(1);
}
/**
* Count how many results are unknown (state=0).
*
* @return int
*/
public function assignsubmission_dta_unknown_count(): int {
return $this->assignsubmission_dta_state_occurence_count(0);
}
/**
* Computes the success rate as a percentage of all results (0..100).
* Note: This includes tests that might have compile errors or unknown states.
*
* @return float A floating percentage between 0.0 and 100.0.
*/
public function assignsubmission_dta_success_rate(): float {
$count = $this->assignsubmission_dta_result_count();
if ($count === 0) {
return 0.0;
}
$successful = $this->assignsubmission_dta_successful_count();
return ($successful / $count) * 100.0;
}
}
dta/classes/privacy/provider.php
+ 25
- 1
  • View file @ 1fb6dbf6

  • Edit in single-file editor

  • Open in Web IDE


@@ -16,6 +16,8 @@
namespace assignsubmission_dta\privacy;
use assign_submission_dta;
use assignsubmission_dta\dta_db_utils;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\writer;
use core_privacy\local\request\contextlist;
@@ -57,6 +59,7 @@ class provider implements \core_privacy\local\metadata\provider,
'global_stacktrace' => 'privacy:metadata:assignsubmission_dta_summary:global_stacktrace',
'successful_competencies' => 'privacy:metadata:assignsubmission_dta_summary:successful_competencies',
'tested_competencies' => 'privacy:metadata:assignsubmission_dta_summary:tested_competencies',
],
'privacy:metadata:assignsubmission_dta_summary'
);
@@ -80,6 +83,20 @@ class provider implements \core_privacy\local\metadata\provider,
'privacy:metadata:assignsubmission_dta_result'
);
$collection->add_database_table(
'assignsubmission_dta_recommendations',
[
'assignmentid' => 'privacy:metadata:assignsubmission_dta_summary:assignmentid',
'submissionid' => 'privacy:metadata:assignsubmission_dta_summary:submissionid',
'topic' => 'privacy:metadata:assignsubmission_dta_recommendations:topic',
'exercise_name' => 'privacy:metadata:assignsubmission_dta_recommendations:exercise_name',
'url' => 'privacy:metadata:assignsubmission_dta_recommendations:url',
'difficulty' => 'privacy:metadata:assignsubmission_dta_recommendations:difficulty',
'score' => 'privacy:metadata:assignsubmission_dta_recommendations:score',
],
'privacy:metadata:assignsubmission_dta_recommendations'
);
$collection->add_external_location_link('dta_backend', [
'assignmentid' => 'privacy:metadata:assignsubmission_dta_summary:assignmentid',
'submissionid' => 'privacy:metadata:assignsubmission_dta_summary:submissionid',
@@ -138,7 +155,7 @@ class provider implements \core_privacy\local\metadata\provider,
$files = get_files($submission, $user);
foreach ($files as $file) {
$userid = $exportdata->get_pluginobject()->userid;
$dtaresultsummary = DBUtils::getresultsummaryfromdatabase($assign->id, $submission->id);
$dtaresultsummary = dta_db_utils::dta_get_result_summary_from_database($assign->id, $submission->id);
// Submitted file.
writer::with_context($exportdata->get_context())->export_file($exportdata->get_subcontext(), $file)
// DTA result.
@@ -173,6 +190,8 @@ class provider implements \core_privacy\local\metadata\provider,
// Delete records from assignsubmission_dta tables.
$DB->delete_records('assignsubmission_dta_result', ['assignmentid' => $assignmentid]);
$DB->delete_records('assignsubmission_dta_summary', ['assignmentid' => $assignmentid]);
$DB->delete_records('assignsubmission_dta_recommendations', ['assignmentid' => $assignmentid]);
}
/**
@@ -201,6 +220,10 @@ class provider implements \core_privacy\local\metadata\provider,
'assignmentid' => $assignmentid,
'submissionid' => $submissionid,
]);
$DB->delete_records('assignsubmission_dta_recommendations', [
'assignmentid' => $assignmentid,
'submissionid' => $submissionid,
]);
}
/**
@@ -228,6 +251,7 @@ class provider implements \core_privacy\local\metadata\provider,
$params['assignid'] = $deletedata->get_assignid();
$DB->delete_records_select('assignsubmission_dta_result', "assignmentid = :assignid AND submissionid $sql", $params);
$DB->delete_records_select('assignsubmission_dta_summary', "assignmentid = :assignid AND submissionid $sql", $params);
$DB->delete_records_select('assignsubmission_dta_recommendations', "assignmentid = :assignid AND submissionid $sql", $params);
}
/**
dta/classes/dta_backend_utils.php 0 → 100644
+ 167
- 0
  • View file @ 1fb6dbf6

  • Edit in single-file editor

  • Open in Web IDE

<?php
// This file is part of Moodle - http://moodle.org/.
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains the backend webservice contact functionality for the DTA plugin.
*
* @package assignsubmission_dta
* @copyright 2023 Your Name
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace assignsubmission_dta;
/**
* Backend webservice contact utility class.
*
* @package assignsubmission_dta
* @copyright 2023 Your Name
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dta_backend_utils {
/**
* Component name for the plugin.
*/
public const ASSIGNSUBMISSION_DTA_COMPONENT_NAME = 'assignsubmission_dta';
/**
* Returns the base URL of the backend webservice as configured in the administration settings.
*
* @return string Backend host base URL.
*/
private static function assignsubmission_dta_get_backend_baseurl(): string {
$backendaddress = get_config(
self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME,
'backendHost'
);
if (empty($backendaddress)) {
\core\notification::error(
get_string('backendHost_not_set', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
);
}
return $backendaddress;
}
/**
* Sends the configuration text file uploaded by the teacher to the backend.
*
* @param \assign $assignment Assignment this test-config belongs to.
* @param \stored_file $file Uploaded test-config.
* @return bool True if no error occurred.
*/
public static function assignsubmission_dta_send_testconfig_to_backend($assignment, $file): bool {
$backendaddress = self::assignsubmission_dta_get_backend_baseurl();
if (empty($backendaddress)) {
return true;
}
// Set endpoint for test upload.
$url = $backendaddress . '/v1/unittest';
// Prepare params.
$params = [
'unitTestFile' => $file,
'assignmentId' => $assignment->get_instance()->id,
];
// If request returned null, return false to indicate failure.
if (is_null(self::assignsubmission_dta_post($url, $params))) {
return false;
} else {
return true;
}
}
/**
* Sends submission config or archive to backend to be tested.
*
* @param \assign $assignment Assignment for the submission.
* @param int $submissionid Submission ID of the current file.
* @param \stored_file $file Submission config file or archive with submission.
* @return string|null JSON string with test results or null on error.
*/
public static function assignsubmission_dta_send_submission_to_backend(
$assignment,
int $submissionid,
$file
): ?string {
$backendaddress = self::assignsubmission_dta_get_backend_baseurl();
if (empty($backendaddress)) {
return null;
}
// Set endpoint for submission upload.
$url = $backendaddress . '/v1/task/' . $submissionid;
// Prepare params.
$params = [
'taskFile' => $file,
'assignmentId' => $assignment->get_instance()->id,
];
return self::assignsubmission_dta_post($url, $params);
}
/**
* Posts the given params to the given URL and returns the response as a string.
*
* @param string $url Full URL to request.
* @param array $params Parameters for HTTP request.
* @return string|null Received body on success or null on error.
*/
private static function assignsubmission_dta_post(string $url, array $params): ?string {
if (!isset($url) || !isset($params)) {
return null;
}
$options = ['CURLOPT_RETURNTRANSFER' => true];
$curl = new \curl();
$response = $curl->post($url, $params, $options);
// Check state of request, if response code is 2xx, return the answer.
$info = $curl->get_info();
if ($info['http_code'] >= 200 && $info['http_code'] < 300) {
return $response;
}
// Something went wrong, return null and display an error message.
$msg = self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME
. ': Post file to server was not successful. HTTP code='
. $info['http_code'];
debugging($msg);
if ($info['http_code'] >= 400 && $info['http_code'] < 500) {
\core\notification::error(
get_string('http_client_error_msg', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
);
return null;
} else if ($info['http_code'] >= 500 && $info['http_code'] < 600) {
\core\notification::error(
get_string('http_server_error_msg', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
);
return null;
} else {
$unknownmsg = get_string('http_unknown_error_msg', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
. $info['http_code'] . ' ' . $response;
\core\notification::error($unknownmsg);
return null;
}
}
}
Assignee
mamunozgil's avatar
mamunozgil
Assign to
2 Reviewers
Khalani's avatar
Khalani
Kurzenberger's avatar
Kurzenberger
Request review from
Labels
0
None
0
None
    Assign labels
  • Manage project labels

Milestone
No milestone
None
None
Time tracking
No estimate or time spent
Lock merge request
Unlocked
3
3 participants
Khalani
Kurzenberger
mamunozgil
Reference: cota/moodle-assignsubmission_dta!1
Source branch: niklas-dev-recommendation

Menu

Explore Projects Groups Snippets

Dies ist die Gitlab-Instanz des Transferportals der Hochschule für Technik Stuttgart. Hier geht es zurück zum Portal