Commit 64cc51fa authored by Kurzenberger's avatar Kurzenberger
Browse files

fixed version with enhanced provider.php and checked with codeChecker

parent 4e16f800
1 merge request!1Coding style and recommendations
Pipeline #10777 passed with stage
Showing with 206 additions and 238 deletions
+206 -238
......@@ -81,7 +81,7 @@ class dta_backend_utils {
];
// If request returned null, return false to indicate failure.
if (is_null(self::dta_post($url, $params))) {
if (is_null(self::assignsubmission_dta_post($url, $params))) {
return false;
} else {
return true;
......
File moved
......@@ -26,7 +26,6 @@ use assignsubmission_dta\models\dta_recommendation;
* Utility class for DTA submission plugin result display.
*
* @package assignsubmission_dta
* @copyright 2023 Your Name <you@example.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dta_view_submission_utils {
......@@ -37,69 +36,96 @@ class dta_view_submission_utils {
public const ASSIGNSUBMISSION_DTA_COMPONENT_NAME = 'assignsubmission_dta';
/**
* Generates a short summary HTML.
* Generates a short summary HTML (like your old plugin).
*
* @param int $assignmentid The assignment ID.
* @param int $submissionid The submission ID to create a report for.
* @return string HTML summary.
* @return string The HTML summary.
*/
public static function assignsubmission_dta_generate_summary_html(
public static function assignsubmission_dta_generate_summary_html(
int $assignmentid,
int $submissionid
): string {
// Fetch data.
$summary = dta_db_utils::assignsubmission_dta_get_result_summary_from_database(
$assignmentid,
$submissionid
);
): string {
// 1) Retrieve the summary data from the DB (adjust your DB-utils class as needed).
$summary = dta_db_utils::assignsubmission_dta_get_result_summary_from_database($assignmentid, $submissionid);
// 2) Prepare an HTML buffer.
$html = '';
// Calculate success rate, if no unknown result states or compilation errors.
$successrate = '?';
if ($summary->unknown_count() === 0 && $summary->compilation_error_count() === 0) {
$successrate = round(($summary->successful_count() / $summary->result_count()) * 100, 2);
}
// 3) Extract counts from your new method names:
$unknowncount = $summary->assignsubmission_dta_unknown_count();
$compilecount = $summary->assignsubmission_dta_compilation_error_count();
$successcount = $summary->assignsubmission_dta_successful_count();
$failcount = $summary->assignsubmission_dta_failed_count();
$totalcount = $summary->assignsubmission_dta_result_count();
// Generate HTML.
$html .= $summary->successful_count() . '/';
if ($summary->compilation_error_count() === 0 && $summary->unknown_count() === 0) {
$html .= $summary->result_count() . ' (' . $successrate . '%)';
// 4) Compute success rate if no unknown/compile errors and total>0.
$successrate = '?';
if ($unknowncount === 0 && $compilecount === 0 && $totalcount > 0) {
$successrate = round(($successcount / $totalcount) * 100, 2);
}
// 5) “X/Y (Z%) tests successful” line:
// If either compile errors or unknown exist -> we show "?"
// else X/Y (rate%).
$html .= $successcount . '/';
if ($compilecount === 0 && $unknowncount === 0) {
$html .= ($totalcount > 0)
? ($totalcount . ' (' . $successrate . '%)')
: ('0 (' . $successrate . ')');
} else {
$html .= '?';
}
$html .= get_string('tests_successful', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME) . '<br />';
$html .= get_string('tests_successful', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME) . "<br />";
if ($summary->compilation_error_count() > 0) {
$html .= $summary->compilation_error_count()
// 6) If there are compilation errors, show them:
if ($compilecount > 0) {
$html .= $compilecount
. get_string('compilation_errors', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
. '<br />';
. "<br />";
}
if ($summary->unknown_count() > 0) {
$html .= $summary->unknown_count()
// 7) If there are unknown results, show them:
if ($unknowncount > 0) {
$html .= $unknowncount
. get_string('unknown_state', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
. '<br />';
. "<br />";
}
// 8) Competencies (like your old snippet):
$showncompetencies = explode(';', $summary->successfultestcompetencies);
$overallcompetencies = explode(';', $summary->overalltestcompetencies);
$tmp = '';
for ($index = 0, $size = count($showncompetencies); $index < $size; $index++) {
$shown = $showncompetencies[$index];
$comp = $overallcompetencies[$index];
// If the competency was actually assessed, add a summary entry.
$size = count($showncompetencies);
for ($i = 0; $i < $size; $i++) {
$shown = $showncompetencies[$i];
$comp = $overallcompetencies[$i];
// If the competency was actually used (non-zero?), show a row.
if ($shown !== '0') {
$tmp .= get_string('comp' . $index, self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
. ' ' . (100 * floatval($shown) / floatval($comp)) . '% ' . '<br />';
$shownval = floatval($shown);
$compval = floatval($comp);
// Guard division by zero:
$pct = 0;
if ($compval > 0) {
$pct = 100.0 * $shownval / $compval;
}
// “compX XX%<br />”
$tmp .= get_string('comp' . $i, self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
. ' ' . round($pct, 2) . '%<br />';
}
}
$html .= get_string('success_competencies', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
. '<br />' . $tmp . '<br />';
. "<br />" . $tmp . "<br />";
return \html_writer::div($html, 'dtaSubmissionSummary');
}
// 9) Wrap it in a DIV for styling, and return.
return \html_writer::div($html, "dtaSubmissionSummary");
}
/**
* Generates detailed view HTML.
......@@ -134,7 +160,7 @@ class dta_view_submission_utils {
$compilationerrorattributes = 'dtaResultCompilationError';
$attributes = ['class' => 'dtaTableData'];
// Building summary table.
// Build the summary table header.
$tmp = \html_writer::tag(
'th',
get_string('summary', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME),
......@@ -146,6 +172,13 @@ class dta_view_submission_utils {
$body = '';
// Pull the counters from the summary object.
$resultcount = $summary->assignsubmission_dta_result_count();
$successfulcount = $summary->assignsubmission_dta_successful_count();
$failedcount = $summary->assignsubmission_dta_failed_count();
$compilationcount = $summary->assignsubmission_dta_compilation_error_count();
$unknowncount = $summary->assignsubmission_dta_unknown_count();
// Total items.
$tmp = '';
$tmp .= \html_writer::tag(
......@@ -153,8 +186,9 @@ class dta_view_submission_utils {
get_string('total_items', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME),
$attributes
);
$tmp .= \html_writer::tag('td', $summary->result_count(), $attributes);
$tmp .= \html_writer::tag('td', $resultcount, $attributes);
$resultrowattributes = $tablerowattributes;
// Original code colors this row as unknown by default:
$resultrowattributes['class'] .= ' ' . $unknownattributes;
$body .= \html_writer::tag('tr', $tmp, $resultrowattributes);
......@@ -165,16 +199,13 @@ class dta_view_submission_utils {
get_string('tests_successful', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME),
$attributes
);
$tmp .= \html_writer::tag('td', $summary->successful_count(), $attributes);
$tmp .= \html_writer::tag('td', $successfulcount, $attributes);
$resultrowattributes = $tablerowattributes;
// Compute success rate if no unknown or compilation errors, and resultcount > 0.
$successrate = '?';
if ($summary->unknown_count() > 0 || $summary->compilation_error_count() > 0) {
$resultrowattributes['class'] .= ' ' . $unknownattributes;
} else {
$successrate = round(
($summary->successful_count() / $summary->result_count()) * 100,
2
);
if ($unknowncount == 0 && $compilationcount == 0 && $resultcount > 0) {
$successrate = round(($successfulcount / $resultcount) * 100, 2);
if ($successrate < 50) {
$resultrowattributes['class'] .= ' ' . $compilationerrorattributes;
} else if ($successrate < 75) {
......@@ -182,6 +213,9 @@ class dta_view_submission_utils {
} else {
$resultrowattributes['class'] .= ' ' . $successattributes;
}
} else {
// If unknown or compilation errors => highlight as unknown.
$resultrowattributes['class'] .= ' ' . $unknownattributes;
}
$body .= \html_writer::tag('tr', $tmp, $resultrowattributes);
......@@ -192,9 +226,9 @@ class dta_view_submission_utils {
get_string('failures', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME),
$attributes
);
$tmp .= \html_writer::tag('td', $summary->failed_count(), $attributes);
$tmp .= \html_writer::tag('td', $failedcount, $attributes);
$resultrowattributes = $tablerowattributes;
if ($summary->failed_count() > 0) {
if ($failedcount > 0) {
$resultrowattributes['class'] .= ' ' . $failureattributes;
} else {
$resultrowattributes['class'] .= ' ' . $successattributes;
......@@ -208,9 +242,9 @@ class dta_view_submission_utils {
get_string('compilation_errors', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME),
$attributes
);
$tmp .= \html_writer::tag('td', $summary->compilation_error_count(), $attributes);
$tmp .= \html_writer::tag('td', $compilationcount, $attributes);
$resultrowattributes = $tablerowattributes;
if ($summary->compilation_error_count() > 0) {
if ($compilationcount > 0) {
$resultrowattributes['class'] .= ' ' . $compilationerrorattributes;
} else {
$resultrowattributes['class'] .= ' ' . $successattributes;
......@@ -224,68 +258,61 @@ class dta_view_submission_utils {
get_string('unknown_state', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME),
$attributes
);
$tmp .= \html_writer::tag('td', $summary->unknown_count(), $attributes);
$tmp .= \html_writer::tag('td', $unknowncount, $attributes);
$resultrowattributes = $tablerowattributes;
if ($summary->unknown_count() > 0) {
if ($unknowncount > 0) {
$resultrowattributes['class'] .= ' ' . $unknownattributes;
} else {
$resultrowattributes['class'] .= ' ' . $successattributes;
}
$body .= \html_writer::tag('tr', $tmp, $resultrowattributes);
// Success rate.
// Success rate row.
$tmp = '';
$tmp .= \html_writer::tag(
'td',
\html_writer::tag(
'b',
get_string('success_rate', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)
),
\html_writer::tag('b', get_string('success_rate', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME)),
$attributes
);
$suffix = '?';
if ($summary->compilation_error_count() === 0 && $summary->unknown_count() === 0) {
$suffix = $summary->result_count() . ' (' . $successrate . '%)';
}
// If no compilation errors or unknown => show successrate, else "?".
$suffix = ($compilationcount == 0 && $unknowncount == 0 && $resultcount > 0)
? ($resultcount . ' (' . $successrate . '%)')
: '?';
$tmp .= \html_writer::tag(
'td',
\html_writer::tag(
'b',
$summary->successful_count() . '/' . $suffix
),
\html_writer::tag('b', $successfulcount . '/' . $suffix),
$attributes
);
$resultrowattributes = $tablerowattributes;
if ($summary->unknown_count() > 0 || $summary->compilation_error_count() > 0) {
$resultrowattributes['class'] .= ' ' . $unknownattributes;
} else {
if ($compilationcount == 0 && $unknowncount == 0 && $resultcount > 0) {
if ($successrate !== '?' && $successrate < 50) {
$resultrowattributes['class'] .= ' ' . $compilationerrorattributes;
} else if ($successrate !== '?' && $successrate < 75) {
$resultrowattributes['class'] .= ' ' . $failureattributes;
} else if ($successrate !== '?') {
} else {
$resultrowattributes['class'] .= ' ' . $successattributes;
}
} else {
$resultrowattributes['class'] .= ' ' . $unknownattributes;
}
$body .= \html_writer::tag('tr', $tmp, $resultrowattributes);
// Finalize the summary table.
$body = \html_writer::tag('tbody', $body);
$table = \html_writer::tag('table', $header . $body, ['class' => 'dtaTable']);
$html .= $table;
// Add empty div for spacing after summary.
// Spacing after the summary table.
$html .= \html_writer::empty_tag('div', ['class' => 'dtaSpacer']);
// *** Recommendations Table ***
if (!empty($recommendations)) {
// Sorting logic.
$allowedsortfields = ['topic', 'exercise_name', 'difficulty', 'score'];
$allowedsortdirs = ['asc', 'desc'];
$sortby = isset($_POST['sortby']) ? $_POST['sortby'] : 'score';
$sortdir = isset($_POST['sortdir']) ? $_POST['sortdir'] : 'asc';
$sortby = $_POST['sortby'] ?? 'score';
$sortdir = $_POST['sortdir'] ?? 'asc';
if (!in_array($sortby, $allowedsortfields)) {
$sortby = 'score';
......@@ -307,7 +334,6 @@ class dta_view_submission_utils {
if ($comparison === 0) {
return 0;
}
if ($sortdir === 'asc') {
return ($comparison < 0) ? -1 : 1;
} else {
......@@ -358,7 +384,7 @@ class dta_view_submission_utils {
return \html_writer::tag('th', $form, ['class' => $class]);
};
// Table header for recommendations.
// Build the recommendations table header.
$tableheader = '';
$tableheader .= $generatesortableheader(
'topic',
......@@ -405,7 +431,7 @@ class dta_view_submission_utils {
$html .= \html_writer::tag('table', $tableheader . $tablebody, ['class' => 'dtaTable']);
// Add empty div for spacing after recommendations.
// Spacing after recommendations.
$html .= \html_writer::empty_tag('div', ['class' => 'dtaSpacer']);
}
......@@ -427,8 +453,18 @@ class dta_view_submission_utils {
for ($index = 0, $size = count($overallcompetencies); $index < $size; $index++) {
$comp = $overallcompetencies[$index];
$shown = $showncompetencies[$index];
// If the competency was assessed, add a row in the table.
// If the competency was actually assessed, add a row in the table.
if ($comp !== '0') {
$compval = floatval($comp);
$shownval = floatval($shown);
// Guard division by zero:
$pct = 0;
if ($compval > 0) {
$pct = (100.0 * $shownval / $compval);
}
$resultrowattributes = $tablerowattributes;
$tmp = '';
$tmp .= \html_writer::tag(
......@@ -438,7 +474,7 @@ class dta_view_submission_utils {
);
$tmp .= \html_writer::tag(
'td',
(100 * floatval($shown) / floatval($comp)) . '% (' . $shown . ' / ' . $comp . ')',
round($pct, 2) . '% (' . $shown . ' / ' . $comp . ')',
$resultrowattributes
);
$tmp .= \html_writer::tag(
......@@ -521,7 +557,7 @@ class dta_view_submission_utils {
);
$body .= \html_writer::tag('tr', $tmp, $resultrowattributes);
// If state is different than successful, show additional info.
// If state != 1, show additional info.
if ($r->state !== 1) {
$tmp = '';
$tmp .= \html_writer::tag(
......
<?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 summary.
*
* @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 summary.
*
* @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_summary {
/**
* @var int $timestamp Timestamp for ordering and deletion of previous results.
*/
public $timestamp;
/**
* @var string $globalstacktrace Global stack trace if applicable, empty otherwise.
*/
public $globalstacktrace;
/**
* @var string $successfultestcompetencies Successfully tested competencies (tests and weights), or empty string.
*/
public $successfultestcompetencies;
/**
* @var string $overalltestcompetencies Overall tested competencies (tests and weights), or empty string.
*/
public $overalltestcompetencies;
/**
* @var array $results List of detail results.
*/
public $results;
/**
* Decodes the JSON result summary returned by the backend service call into the plugin PHP data structure.
*
* @param string $jsonstring JSON string containing DtaResultSummary.
* @return dta_result_summary The 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;
$summary->globalstacktrace = $response->globalstacktrace;
$summary->timestamp = $response->timestamp ?? 0;
$summary->globalstacktrace = $response->globalstacktrace ?? '';
$summary->successfultestcompetencies = $response->successfulTestCompetencyProfile ?? '';
$summary->overalltestcompetencies = $response->overallTestCompetencyProfile ?? '';
if (!empty($response->results) && is_array($response->results)) {
$summary->results = self::assignsubmission_dta_decode_json_result_array($response->results);
} else {
$summary->results = [];
}
return $summary;
}
/**
* Decodes an array of JSON detail results into the plugin PHP data structure.
*
* @param array $jsonarray Decoded JSON array of results.
* @return array Array of dta_result objects.
*/
private static function assignsubmission_dta_decode_json_result_array(array $jsonarray): array {
$ret = [];
foreach ($jsonarray as $entry) {
......@@ -96,30 +42,19 @@ class dta_result_summary {
$value->failuretype = $entry->failureType ?? '';
$value->failurereason = $entry->failureReason ?? '';
$value->stacktrace = $entry->stacktrace ?? '';
$value->columnnumber = $entry->columnNumber ?? '';
$value->linenumber = $entry->lineNumber ?? '';
$value->position = $entry->position ?? '';
$value->columnnumber = $entry->columnNumber ?? 0;
$value->linenumber = $entry->lineNumber ?? 0;
$value->position = $entry->position ?? 0;
$ret[] = $value;
}
return $ret;
}
/**
* Returns the number of detail results attached to the summary.
*
* @return int Count of occurrences.
*/
public function assignsubmission_dta_result_count(): int {
return count($this->results);
}
/**
* Returns the number of detail results with the given state attached to the summary.
*
* @param int $state State ordinal number.
* @return int Count of occurrences for the provided state.
*/
public function assignsubmission_dta_state_occurence_count(int $state): int {
$num = 0;
foreach ($this->results as $r) {
......@@ -130,39 +65,29 @@ class dta_result_summary {
return $num;
}
/**
* Returns the number of detail results with compilation errors attached to the summary.
*
* @return int Count of occurrences.
*/
public function assignsubmission_dta_compilation_error_count(): int {
return $this->assignsubmission_dta_state_occurence_count(3);
return $this->assignsubmission_dta_state_occurence_count(3); // State=3 => compile error
}
/**
* Returns the number of detail results with test failures attached to the summary.
*
* @return int Count of occurrences.
*/
public function assignsubmission_dta_failed_count(): int {
return $this->assignsubmission_dta_state_occurence_count(2);
return $this->assignsubmission_dta_state_occurence_count(2); // State=2 => fail
}
/**
* Returns the number of detail results with successful tests attached to the summary.
*
* @return int Count of occurrences.
*/
public function assignsubmission_dta_successful_count(): int {
return $this->assignsubmission_dta_state_occurence_count(1);
return $this->assignsubmission_dta_state_occurence_count(1); // State=1 => success
}
/**
* Returns the number of detail results with an unknown result attached to the summary.
*
* @return int Count of occurrences.
*/
public function assignsubmission_dta_unknown_count(): int {
return $this->assignsubmission_dta_state_occurence_count(0);
return $this->assignsubmission_dta_state_occurence_count(0); // State=0 => unknown
}
// OPTIONAL: A helper to safely get success rate 0..100
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;
}
}
......@@ -190,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]);
}
/**
......@@ -218,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,
]);
}
/**
......@@ -245,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);
}
/**
......
......@@ -55,7 +55,7 @@ class assign_submission_dta extends assign_submission_plugin {
*
* @return string
*/
public function assignsubmission_dta_get_name(): string {
public function get_name(): string {
return get_string('pluginname', self::ASSIGNSUBMISSION_DTA_COMPONENT_NAME);
}
......@@ -65,7 +65,7 @@ class assign_submission_dta extends assign_submission_plugin {
* @param MoodleQuickForm $mform Form to add elements to.
* @return void
*/
public function assignsubmission_dta_get_settings(MoodleQuickForm $mform): void {
public function get_settings(MoodleQuickForm $mform): void {
// Add draft filemanager to form.
$mform->addElement(
'filemanager',
......@@ -131,7 +131,7 @@ class assign_submission_dta extends assign_submission_plugin {
* @param stdClass $data Form data.
* @return bool
*/
public function assignsubmission_dta_save_settings(stdClass $data): bool {
public function save_settings(stdClass $data): bool {
// If the assignment has no filemanager for our plugin, just leave.
$draftfilemanagerid = self::ASSIGNSUBMISSION_DTA_DRAFT_FILEAREA_TEST;
if (!isset($data->$draftfilemanagerid)) {
......@@ -232,8 +232,8 @@ class assign_submission_dta extends assign_submission_plugin {
* @param stdClass $submission Submission to check.
* @return bool True if file count is zero.
*/
public function assignsubmission_dta_is_empty(stdClass $submission): bool {
return ($this->assignsubmission_dta_count_files(
public function is_empty(stdClass $submission): bool {
return ($this->count_files(
$submission->id,
self::ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION
) === 0);
......@@ -246,7 +246,7 @@ class assign_submission_dta extends assign_submission_plugin {
* @param string $areaid Filearea id to count.
* @return int Number of files submitted in the filearea.
*/
private function assignsubmission_dta_count_files(int $submissionid, $areaid): int {
private function count_files(int $submissionid, $areaid): int {
$fs = get_file_storage();
$files = $fs->get_area_files(
$this->assignment->get_context()->id,
......@@ -279,7 +279,7 @@ class assign_submission_dta extends assign_submission_plugin {
);
// If submission is empty, leave directly.
if ($this->assignsubmission_dta_is_empty($submission)) {
if ($this->is_empty($submission)) {
return true;
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment