I have notes for classes I teach (‘type = class_instance’) and notes for students who have attended classes (‘type = student’). The student notes have a frontmatter field ‘classes_attended’ where I manually add the ‘class_instance_id’ value of each class they’ve attended (I’m open to tweaking this part of the workflow if needed).
I’m attempting to create a process (auto-updating or one-time check when opening the note, but a button would work too) for the ‘class_instance’ notes that would:
Read the current note’s ‘class_instance_id’ from frontmatter.
Scan all notes with ‘type: student’.
Find the student notes whose ‘classes_taken’ field includes the current note’s ‘class_instance_id’ (‘classes_taken’ field may also include other ‘class_instance_id’ values if they’ve taken multiple classes, this field is structured as a list).
Collect those students’ ‘student_id’ values (or note titles) and write them as separate entries into the current note’s ‘students_attended’ (another list) frontmatter.
(Optional, but would really like to have, maybe a separate question) Add up the number of matching student notes and write the number into the field ‘number_of_students’.
I’ve searched community plugins and this forum, I’ve asked ChatGPT, I’ve tried using Templater, Metabind, and some JS but everything has given me errors and I’m stuck. If this is possible, what is the best way to go about it? What plugins do I need, what methods should I use? I really appreciate any help you can give me!
All of these steps can be handled by a Templater script using JS and the Obsidian API, though it does require some effort. This might be helpful: Templater snippets.
Another option is to use the Bases plugin. You can embed a base for each class and filter all students attending that class by using the ID. You may need to slightly adjust your workflow, but Bases should do this well. I’m doing something similar with cross-referencing a storage system via IDs, and it works.
Both. Bases can make use of recursive formulas using this. to access properties of the notes they are embedded in. So you would create a base and filter it for students who have file.properties.classes_taken.contains(this.class_instance_id) (I did not test it, so maybe not 100% correct syntax, just as an idea). When you embed this base in any of your class notes, it will give you a view showing all students who attended that class. This provides an automatic overview of what you want, as far as I understand.
In Bases table view you can also edit frontmatter properties. With multi-row editing, including copying and pasting columns, you should be able to quickly add a taken class to multiple students at once.
And in case this helps someone else, I got the other part working too. My student notes have a ‘classes_taken’ field. I manually fill that in as students sign up. My class notes have a ‘students_attended’ field and a ‘number_of_students’ field. I made a QuickAdd macro (and then a meta-bind button to run it) that updates those two fields in the class note based on which students have taken that class. Here’s the js:
module.exports = async (params) => {
const { app } = params;
// Get the current file and its frontmatter
const file = app.workspace.getActiveFile();
const cache = app.metadataCache.getFileCache(file);
const classId = cache?.frontmatter?.class_instance_id;
// Exit if no class_instance_id is found
if (!classId) {
new Notice("No class_instance_id found");
return;
}
// Find all student files by type
const studentFiles = app.vault.getMarkdownFiles().filter((f) => {
const cache = app.metadataCache.getFileCache(f);
return cache?.frontmatter?.type === "student";
});
const students = [];
// Loop through each student file
for (const studentFile of studentFiles) {
const studentCache = app.metadataCache.getFileCache(studentFile);
// Check if this student has taken this class
if (studentCache?.frontmatter?.classes_taken?.includes(classId)) {
// Add the student's filename to the list
students.push(studentFile.basename);
}
}
// Read the current file's content
let content = await app.vault.read(file);
const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
const match = content.match(frontmatterRegex);
if (match) {
let frontmatter = match[1];
// Prepare the students list in YAML list format
const studentsList =
students.length > 0
? "\n" + students.map((s) => ` - ${s}`).join("\n")
: " []";
// Count of students
const studentCount = students.length;
// Check if students_attended already exists
if (/^students_attended:/m.test(frontmatter)) {
// Replace existing field in place (preserves position)
// This regex matches the field and all following indented lines
frontmatter = frontmatter.replace(
/^students_attended:.*$(\n -.*$)*/gm,
`students_attended:${studentsList}`,
);
} else {
// Add new field after materials_version if it exists
if (/^materials_version:/m.test(frontmatter)) {
frontmatter = frontmatter.replace(
/^materials_version:.*$/m,
`$&\nstudents_attended:${studentsList}`,
);
} else {
// Otherwise add at the end
frontmatter += `\nstudents_attended:${studentsList}`;
}
}
// Update number_of_students field
if (/^number_of_students:/m.test(frontmatter)) {
// Replace existing number in place (preserves position)
frontmatter = frontmatter.replace(
/^number_of_students:.*$/m,
`number_of_students: ${studentCount}`,
);
} else {
// Add new field after students_attended
frontmatter = frontmatter.replace(
/^students_attended:.*$(\n -.*$)*/gm,
`$&\nnumber_of_students: ${studentCount}`,
);
}
// Replace the frontmatter in the content
content = content.replace(frontmatterRegex, `---\n${frontmatter}\n---`);
// Write the updated content back to the file
await app.vault.modify(file, content);
// Show success notification
new Notice(`✅ Updated: ${students.length} students`);
}
};