Giving Feedback to Student Texts with R and Obsidian Templates

This isn’t right, this isn’t even wrong.
Wolfgang Pauli (1900-1958), upon reading a young physicist’s paper

Part of the job is giving feedback to student writing. It’s better they get feedback before they write 40-100 pages in their bachelor thesis. Still, it’s a rather thankless job … and oftentimes somewhat annoying when you have to correct the same mistakes over and over again.

After trying different ways to give feedback, I tried using Obsidian.

As Obsidian can use templates, and inserts them where ever you put the cursor, this seemed like an easy way to deal with frequent mistakes.

I did create page first containing the most frequent errors I did encounter in previous semesters.

 

The template list with text blocks to insert is rather long, but using a clear categorization made finding the text block very easy.

Using the Highlightr community plugin to mark feedback in the text (highlighted, green for template text that might not fit 100%, but addresses the underlying issue, purple for other comments that are not based on templates).

Example for a text block. The tone is a bit … biting at times. And yeah, it’s in German.

Obsidian itself in Light color scheme to make exporting the corrections as PDF nicer. If each piece of writing has the author name and whether they did the task successfully or not in dataview syntax (e.g., author:: and result::), dataview can create a nice table showing the student names and whether they passed.

The dataview table got updated with the individual entries.

 

Nice to see when you make progress.

If the student name is at the end of the text, and in a folded in box (e.g., `> [!info]- Author`, the – behind the ] makes the box foldable), you can grade the text not knowing who wrote it. Leads to interesting surprises and more honest/fair feedback.

The Author box was folded in by default. The page itself only got a — randomly assigned — ID (via the R script).

Given that we use Moodle as learning platform, an issue was to get the student submissions into Obsidian. As Moodle can export the texts as HTML, a simple HTML to Markdown converter in R did the trick:

prepareMoodleHTMLForGrading <- function() {
    
    htmlToMarkdown <- function(x) {
        x <- paste0(x, collapse = " ")
        x <- str_replace_all(x, "  ", " ")
        x <- str_replace_all(x, "&nbsp;", " ")
        x <- str_replace_all(x, "</p>", "\n\n")
        x <- str_replace_all(x, "<br>", "\n")
        x <- str_replace_all(x, "\n ", "\n")
        x <- str_replace_all(x, "<b>", "\\*\\*")
        x <- str_replace_all(x, "</b>", "\\*\\*")
        x <- str_replace_all(x, "<i>", "\\*")
        x <- str_replace_all(x, "</i>", "\\*")
        x <- str_replace_all(x, "<u>", "\\*\\*")
        x <- str_replace_all(x, "</u>", "\\*\\*")
        x <- str_replace_all(x, "<h1>", "\n# ")
        x <- str_replace_all(x, "<h2>", "\n## ")
        x <- str_replace_all(x, "<h3>", "\n### ")
        x <- str_replace_all(x, "<h4>", "\n#### ")
        x <- str_replace_all(x, "<h5>", "\n##### ")
        x <- str_replace_all(x, "<h6>", "\n###### ")
        x <- str_replace_all(x, "<[^>]*>", "")
        
        while(str_detect(x, "\n ")) {
            x <- str_replace_all(x, "\n ", "\n")
        }
        while(str_detect(x, " \n")) {
            x <- str_replace_all(x, " \n", "\n")
        }
        while(str_detect(x, "\n\n\n")) {
            x <- str_replace_all(x, "\n\n\n", "\n\n")
        }
        x <- trimws(x)
        return(x)
    }
    
    tasks <- list.dirs("INPUTPATH", recursive = FALSE)
    
    for(i in 1:length(tasks)) {
        curDir <- str_replace(tasks[[i]], "INPUTPATH", "")
        targetDir <- paste0("OBSIDIANPATH", curDir)
        if(!dir.exists(targetDir)) {
            dir.create(targetDir)
        }
        
        submissionPath <- paste0("INPUTPATH", curDir)
        submissions <- list.files(submissionPath, full.names = TRUE, pattern = ".html", recursive = TRUE)
        
        concealIDs <- sample(seq(1:length(submissions)), length(submissions))
        
        for(j in 1:length(submissions)) {
            curSub <- read_lines(submissions[[j]])
            
            curName <- str_replace(submissions[[j]], paste0("INPUTPATH", curDir, "/"), "") 
            curName <- str_split(curName, "_")[[1]][[1]]
            
            outputText <- c(htmlToMarkdown(curSub),
                            "\n\n---\n\n## Allgemeine Bemerkungen\n\n\n---\n\n",
                            paste0("> [!info]- Author\n> Abgabe:: ", curName, "\n> Bewertung:: unbewertet\n> Aufgabe: ", curDir, "\n\n")
            )
            # targetName <- paste0(targetDir, "/", curDir, " ", curName, ".md")
            targetName <- paste0(targetDir, "/", curDir, " ID ", concealIDs[[j]], ".md")
            if(!file.exists(targetName)) {
                write_lines(outputText, targetName, sep = "")
            } else {
                warning(paste0("File ", targetName, " already exists. Skipped."))
            }
        }
        
        gradingTableText <- paste0(curDir, "\n===================\n\n```dataview\nTABLE WITHOUT ID\n  Abgabe, Bewertung\n  FROM \"Korrekturen/", curDir, "\"\n  SORT Abgabe\n```\n\n\n\n> [!info]- Author\n> Abgabe:: 0 Bewertungsbogen\n> Bewertung:: 0 Übersicht\n> Aufgabe: ", curDir, "\n\n\n")
        targetName <- paste0(targetDir, "/0 ", curDir, " Grading Table.md")
        if(!file.exists(targetName)) {
            write_lines(gradingTableText, targetName, sep = "")
        } else {
            warning(paste0("File ", targetName, " already exists. Skipped."))
        }
        
    }
}

The code is not perfect, and I have made changes on the files afterwards that would be better done in the code. But it works, kinda. As usual, no warranty.

As grading (can) suck(s), I took a precaution that the script will not save the recoded HTML to Markdown files if the target file already exists. It also adds some information text below and create the grading overview table.

The result are the files in Obsidian, ready to be corrected. And the table updates when the individual files are updated.

I’ve blurred the student text, it just shows the principle. Green is a text block, purple a free comment written for the specific text.

 

I added the color coding scheme (the think bubble was for a repeating mistake, student has to find out what it means based on what was already given as feedback). Also contains a short paragraph with concluding remarks.

And yeah, with the script in place, I can use it grade other student submissions in the future. While there is (a lot of) room for improvement, the principle works.