Skip to content

Commit

Permalink
Merge pull request #77 from drop-project-edu/76-readmemd-rendering-is…
Browse files Browse the repository at this point in the history
…sue-in-build-report

[Fix] README.md rendering issue in build report - fixes #76
  • Loading branch information
palves-ulht authored Feb 2, 2025
2 parents e24b325 + 3a1f679 commit caa1af2
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 156 deletions.
198 changes: 127 additions & 71 deletions src/main/kotlin/org/dropProject/controllers/ReportController.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ open class DropProjectSecurityConfig(val apiAuthenticationManager: PersonalToken
*getPublicUrls().toTypedArray()
).permitAll()
.antMatchers(
"/", "/upload", "/upload/**", "/buildReport/*", "/student/**",
"/", "/upload", "/upload/**", "/buildReport/**", "/student/**",
"/git-submission/refresh-git/*", "/git-submission/generate-report/*", "/mySubmissions",
"/leaderboard/*",
"/personalToken", "/api/student/**"
Expand All @@ -92,7 +92,7 @@ open class DropProjectSecurityConfig(val apiAuthenticationManager: PersonalToken

if (apiAuthenticationManager != null) {
http.addFilterBefore(PersonalTokenAuthenticationFilter("/api/**", apiAuthenticationManager),
LogoutFilter::class.java)
LogoutFilter::class.java)
}

http.headers().frameOptions().sameOrigin() // this is needed for h2-console
Expand Down
78 changes: 14 additions & 64 deletions src/main/kotlin/org/dropProject/services/AssignmentTeacherFiles.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,38 +59,6 @@ data class AssignmentInstructions(
var body: String? = null
)

/**
* Transforms relative links into absolute links, during the rendering of markdown documents
*/
class RelativeToAbsoluteLinkVisitor(private val baseUrlForLinks: String,
private val baseUrlForImages: String) : AbstractVisitor() {

override fun visit(image: Image) {
val destination = image.destination

// Check if the link is relative
if (!destination.startsWith("http://") && !destination.startsWith("https://")) {
// Convert the relative link to an absolute one
image.destination = baseUrlForImages + destination
}

super.visit(image)
}

override fun visit(link: Link) {
val destination = link.destination

// Check if the link is relative
if (!destination.startsWith("http://") && !destination.startsWith("https://")) {
// Convert the relative link to an absolute one
link.destination = baseUrlForLinks + destination
}

// Proceed with the default behavior for this node
visitChildren(link)
}
}

/**
* Provides functionality related with an Assignment's Teacher Files (for example, checking if the Teacher's submission
* compiles, passes the CheckStyle, and so on).
Expand All @@ -100,7 +68,8 @@ class AssignmentTeacherFiles(val buildWorker: BuildWorker,
val buildReportRepository: BuildReportRepository,
val assignmentTestMethodRepository: AssignmentTestMethodRepository,
val applicationContext: ApplicationContext,
val i18n: MessageSource
val i18n: MessageSource,
val markdownRenderer: MarkdownRenderer,
) {

@Value("\${assignments.rootLocation}")
Expand All @@ -121,28 +90,9 @@ class AssignmentTeacherFiles(val buildWorker: BuildWorker,
val extension = fragment.extension.uppercase()
instructions.format = AssignmentInstructionsFormat.valueOf(extension)
if (extension == "MD") {
val extensions = listOf(AutolinkExtension.create(), TablesExtension.create())
val parser = Parser.builder().extensions(extensions).build();
val document = parser.parse(fragment.readText());

// Create the visitor with the base URL for converting relative links
val visitor = RelativeToAbsoluteLinkVisitor("public/${assignment.id}/", "${assignment.id}/")

// Apply the visitor to the document
document.accept(visitor)

val renderer = HtmlRenderer.builder()
.extensions(extensions)
// custom attribute provider to add 'table' class to tables rendered by commonmark
.attributeProviderFactory { _ ->
AttributeProvider { node, tagName, attributes ->
if (node is TableBlock) {
attributes["class"] = "table table-bordered"
}
}
}
.build();
instructions.body = renderer.render(document)
instructions.body = markdownRenderer.render(fragment.readText(),
"public/${assignment.id}/",
"${assignment.id}/")

// TODO: While the plugin is not able to render markdown, let's just return html
instructions.format = AssignmentInstructionsFormat.HTML
Expand Down Expand Up @@ -219,7 +169,7 @@ class AssignmentTeacherFiles(val buildWorker: BuildWorker,
val buildReport = buildWorker.checkAssignment(assignmentFolder, assignment, principal?.name)
if (buildReport == null) {
report.add(AssignmentValidator.Info(AssignmentValidator.InfoType.ERROR,
"Assignment checking (run tests) was aborted by timeout! Why is it taking so long to run?"))
"Assignment checking (run tests) was aborted by timeout! Why is it taking so long to run?"))
return report
}

Expand All @@ -232,25 +182,25 @@ class AssignmentTeacherFiles(val buildWorker: BuildWorker,
for (testMethod in assignmentValidator.testMethods) {
val parts = testMethod.split(":")
assignmentTestMethodRepository.save(AssignmentTestMethod(assignment = assignment,
testClass = parts[0], testMethod = parts[1]))
testClass = parts[0], testMethod = parts[1]))
}

if (!buildReport.compilationErrors.isEmpty()) {
report.add(AssignmentValidator.Info(AssignmentValidator.InfoType.ERROR,
"Assignment has compilation errors."))
"Assignment has compilation errors."))
return report
}

if (!buildReport.checkstyleErrors.isEmpty()) {
report.add(AssignmentValidator.Info(AssignmentValidator.InfoType.ERROR,
"Assignment has checkstyle errors."))
"Assignment has checkstyle errors."))
return report
}

if (buildReport.hasJUnitErrors() == true) {
report.add(AssignmentValidator.Info(AssignmentValidator.InfoType.ERROR,
"Assignment is failing some JUnit tests. Please fix this!",
"<pre>${buildReport.junitErrorsTeacher}</pre>"))
"Assignment is failing some JUnit tests. Please fix this!",
"<pre>${buildReport.junitErrorsTeacher}</pre>"))
return report
}

Expand All @@ -259,13 +209,13 @@ class AssignmentTeacherFiles(val buildWorker: BuildWorker,

fun getProjectFolderAsFile(submission: Submission, wasRebuilt: Boolean) : File {
val projectFolder =
if (submission.submissionId != null) submission.submissionId
else submission.gitSubmissionId!!.toString()
if (submission.submissionId != null) submission.submissionId
else submission.gitSubmissionId!!.toString()

val suffix = if (wasRebuilt) "-mavenized-for-rebuild" else "-mavenized"

val destinationPartialFolder = File(mavenizedProjectsRootLocation,
Submission.relativeUploadFolder(submission.assignmentId, submission.submissionDate))
Submission.relativeUploadFolder(submission.assignmentId, submission.submissionDate))
destinationPartialFolder.mkdirs()

return File(destinationPartialFolder, projectFolder + suffix)
Expand Down
104 changes: 104 additions & 0 deletions src/main/kotlin/org/dropProject/services/MarkdownRenderer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*-
* ========================LICENSE_START=================================
* DropProject
* %%
* Copyright (C) 2019 Pedro Alves
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =========================LICENSE_END==================================
*/
package org.dropProject.services

import org.commonmark.ext.autolink.AutolinkExtension
import org.commonmark.ext.gfm.tables.TableBlock
import org.commonmark.ext.gfm.tables.TablesExtension
import org.commonmark.node.AbstractVisitor
import org.commonmark.node.Image
import org.commonmark.node.Link
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.AttributeProvider
import org.commonmark.renderer.html.HtmlRenderer
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.util.*

/**
* Transforms relative links into absolute links, during the rendering of markdown documents
*/
class RelativeToAbsoluteLinkVisitor(private val baseUrlForLinks: String,
private val baseUrlForImages: String) : AbstractVisitor() {

override fun visit(image: Image) {
val destination = image.destination

// Check if the link is relative
if (!destination.startsWith("http://") && !destination.startsWith("https://")) {
// Convert the relative link to an absolute one
image.destination = baseUrlForImages + destination
}

super.visit(image)
}

override fun visit(link: Link) {
val destination = link.destination

// Check if the link is relative
if (!destination.startsWith("http://") && !destination.startsWith("https://")) {
// Convert the relative link to an absolute one
link.destination = baseUrlForLinks + destination
}

// Proceed with the default behavior for this node
visitChildren(link)
}
}

/**
* Utility to perform the rendering of markdown files to html.
*/
@Service
class MarkdownRenderer {

val LOG = LoggerFactory.getLogger(this.javaClass.name)

/**
* Transforms markdown content (using github markdown flavor) to html,
* replacing links and images with absolute paths
*/
fun render(markdownContent: String, baseUrlForLinks: String, baseUrlForImages: String) : String {

val extensions = listOf(AutolinkExtension.create(), TablesExtension.create())
val parser = Parser.builder().extensions(extensions).build();
val document = parser.parse(markdownContent);

// Create the visitor with the base URL for converting relative links
val visitor = RelativeToAbsoluteLinkVisitor(baseUrlForLinks, baseUrlForImages)

// Apply the visitor to the document
document.accept(visitor)

val renderer = HtmlRenderer.builder()
.extensions(extensions)
// custom attribute provider to add 'table' class to tables rendered by commonmark
.attributeProviderFactory { _ ->
AttributeProvider { node, tagName, attributes ->
if (node is TableBlock) {
attributes["class"] = "table table-bordered"
}
}
}
.build();
return renderer.render(document)
}
}
14 changes: 7 additions & 7 deletions src/main/kotlin/org/dropProject/services/ReportService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class ReportService(
val buildReportBuilder: BuildReportBuilder,
val i18n: MessageSource,
val gitClient: GitClient,
val asyncConfigurer: AsyncConfigurer
val asyncConfigurer: AsyncConfigurer,
val markdownRenderer: MarkdownRenderer,
) {

val LOG = LoggerFactory.getLogger(this.javaClass.name)
Expand Down Expand Up @@ -132,13 +133,12 @@ class ReportService(
val readmeContent = File(mavenizedProjectFolder, "README.txt").readText()
fullBuildReport.readmeHtml = "<pre>$readmeContent</pre>"
} else if (File(mavenizedProjectFolder, "README.md").exists()) {

val readmeContent = File(mavenizedProjectFolder, "README.md").readText()
val parser = Parser.builder()
.extensions(listOf(AutolinkExtension.create()))
.build()
val document = parser.parse(readmeContent)
val renderer = HtmlRenderer.builder().build()
fullBuildReport.readmeHtml = "<hr/>\n" + renderer.render(document) + "<hr/>\n"
val htmlContent = markdownRenderer.render(readmeContent,
"${submissionId}/",
"${submissionId}/")
fullBuildReport.readmeHtml = "<hr/>\n$htmlContent<hr/>\n"
}

// check the submission status
Expand Down
30 changes: 18 additions & 12 deletions src/main/kotlin/org/dropProject/services/SubmissionService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -133,19 +133,19 @@ class SubmissionService(
val reportElements = submissionReportRepository.findBySubmissionId(lastSubmission.id)
lastSubmission.reportElements = reportElements

val mavenizedProjectFolder = assignmentTeacherFiles.getProjectFolderAsFile(lastSubmission,
lastSubmission.getStatus() == SubmissionStatus.VALIDATED_REBUILT)
val buildReport = buildReportBuilder.build(buildReportDB.buildReport.split("\n"),
mavenizedProjectFolder.absolutePath, assignment, lastSubmission)
lastSubmission.ellapsed = buildReport.elapsedTimeJUnit()
lastSubmission.teacherTests = buildReport.junitSummaryAsObject(TestType.TEACHER)
lastSubmission.hiddenTests = buildReport.junitSummaryAsObject(TestType.HIDDEN)
if (buildReport.jacocoResults.isNotEmpty()) {
lastSubmission.coverage = buildReport.jacocoResults[0].lineCoveragePercent
}
val mavenizedProjectFolder = assignmentTeacherFiles.getProjectFolderAsFile(lastSubmission,
lastSubmission.getStatus() == SubmissionStatus.VALIDATED_REBUILT)
val buildReport = buildReportBuilder.build(buildReportDB.buildReport.split("\n"),
mavenizedProjectFolder.absolutePath, assignment, lastSubmission)
lastSubmission.ellapsed = buildReport.elapsedTimeJUnit()
lastSubmission.teacherTests = buildReport.junitSummaryAsObject(TestType.TEACHER)
lastSubmission.hiddenTests = buildReport.junitSummaryAsObject(TestType.HIDDEN)
if (buildReport.jacocoResults.isNotEmpty()) {
lastSubmission.coverage = buildReport.jacocoResults[0].lineCoveragePercent
}

lastSubmission.testResults = buildReport.testResults()
}
lastSubmission.testResults = buildReport.testResults()
}
}

submissionInfoList.add(SubmissionInfo(group, lastSubmission, sortedSubmissionList))
Expand Down Expand Up @@ -551,6 +551,12 @@ class SubmissionService(
FileUtils.copyFile(File(projectFolder, "README.txt"), File(mavenizedProjectFolder, "README.txt"))
}

// if the students have images in the root folder, copy them as well
projectFolder.
listFiles { file -> file.extension in listOf("png", "jpg", "jpeg", "gif") }
?.forEach { FileUtils.copyFile(it, File(mavenizedProjectFolder, it.name))
}

if (submission.gitSubmissionId == null && deleteOriginalProjectFolder) { // don't delete git submissions
FileUtils.deleteDirectory(projectFolder) // TODO: This seems duplicate with the lines below...
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1248,4 +1248,24 @@ class ReportControllerTests {

}

@Test
@DirtiesContext
fun `download submission asset`() {

val submissionId = testsHelper.uploadProject(this.mvc, "projectWithREADME", defaultAssignmentId, STUDENT_1)

val result = this.mvc.perform(get("/buildReport/$submissionId/cross_red_icon.png")
.with(user(STUDENT_1)))
.andExpect(status().isOk)
.andReturn()

val downloadedFileContent = result.response.contentAsByteArray
assertEquals(199, downloadedFileContent.size)

// inexistent file
this.mvc.perform(get("/buildReport/$submissionId/other.png")
.with(user(STUDENT_1)))
.andExpect(status().isNotFound)
}

}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit caa1af2

Please sign in to comment.