How to create a questions bank?

Hi everyone,

I’m working on a Typst project aimed at creating a question bank that compiles university entrance exams from all regions in Spain. I want to build a modular, reusable, and flexible system to generate customized exam sheets or collect all the questions from a complete module.

Project structure

  • A central file with the preamble, where I define functions like question, section, criteria, answer, and the general document layout.
  • Separate files for each region, acting as databases with all the questions, subdivided into sections, answers, and criteria.
  • A main file where I want to gather specific questions based on filters like year, exam session, region, or module, and optionally toggle the display of answers and evaluation criteria.

What I have so far

  • The file structure mostly set up.
  • The functions question, part, criteria, answer working at a basic level.

What I need help with

  • How can I make each question automatically calculate its total score, by summing the value of the section it contains?
  • Is there a way in Typst to filter questions based on metadata, such as:
    • Year (e.g., "2025")
    • Session (e.g., "Ordinary")
    • Region (e.g., "Andalusia")
    • Module (e.g., "Biomolecules")
  • How can I conditionally show or hide criteria and answers, depending on a global setting (like a variable show-answers: true)?

I know this is quite an ambitious project, but I believe Typst has great potential for educational applications like this.

Here’s the current draft of my document (still very much a work in progress):

https://typst.app/project/rGHm6Xv7SePAKwETQDXqu6

Any advice, ideas, or suggestions on how to approach these problems would be greatly appreciated. Thanks in advance!

2 Likes

I unfortunately don’t have time right now for a detailed answer, but as a starting point I’d look at the packages here: “exam” search results on Universe. Maybe one does what you want or gives you inspiration.

I’m obviously biased, but my own package for this is Scrutinize 0.3.0: author exams with Typst (link goes to the forum announcement). It explicitly supports the broad features you need help with: calculating sums of points, associating questions with metadata for filtering, and supporting answers shown based on a setting.

What makes Scrutinize a bit opinionated is that each question is a heading. This supports subquestions naturally, but I know some people prefer lists and other non-heading structures for organizing their exams.

1 Like

Hi.

Depends on your structure. You either pass data from section to question and sum the scores, or work with metadata, context, and query.

Yes. Same as above, but instead of section to question, there will be question to questions.

Passing global to local is harder, but still: either through context metadata or by returning function with partially passed arguments.

Definitely not in terms of implementation, especially if the simplest-to-implement structure is used.

Here are my questions about the code, there is a lot to discuss. Hopefully this will give some answers on how to improve the documentation.

  • What does this supposed to import?

    #import "ANDALUCÍA/andal_banco.typ": *
    
    #import "ASTURIAS/astur_banco.typ": *
    
  • Is this “institute”?

    inst: none,
    
  • Why uppercase is for text.lang?

    set text(lang: "ES")
    
  • Why fractions are used for vertical spacing instead of length?

    • Why align is wrapped in block?
    • What horizontal alignment is supposed to do?
    • Why text.hyphenate is explicitly added if it’s not enabled by default?
      block(width: 100%,
        align(horizon+center,text(hyphenate: false, weight: "bold", size: 40pt,
          upper(title))
        )
      )
    
  • Why not use centered line with line.length?

      line(start: (25%, 0%), end: (75%, 0%))
    
  • This looks like a hack, use block.spacing.

      v(0.1fr)
    
  • Why 2 specifically? pagebreak.to

      pagebreak()
      pagebreak()
    
  • Why is this here when no headings are used?

      outline()
    
    • Why scoring exists, if total score is supposed to be calculated automatically?
    • Why you need underscores?
    • Why you need 3–4 char long names?
    #let question(
      scoring: 1,
      tag_reg: none,
      tag_year: none,
      tag_ses: none,
      tag_mod: none,
      body,
    ) = {
    
    • Why use block.width?
    • What box(width: 12.6pt,text(weight: "bold", enum(numbering: "1."))) supposed to do?
    • Why first 2 parts are explicitly joined through +, but last one is joined implicitly?
      block(width: 100%, below: 1.6em,
        if scoring == 1 {
          box(width: 12.6pt,text(weight: "bold", enum(numbering: "1.")))+[* (#scoring punto)*]
          body
        } else {
          box(width: 12.6pt,text(weight: "bold", enum(numbering: "1.")))+[* (#scoring puntos)*]
          body
        }
      )
    
  • Do you really benefit from not writing “o”?

      shw: false,
    
    • Why use block’s stroke and inset instead of just rect?
    • Why are you centering a 101% width block? Do you want block.outset?
    • Why horizon is used?
    • Why use text.weight and not strong or **?
    • Why use math symbol over square?
        align(center + horizon, block(width: 101%, stroke: black, inset: (top: 4pt, left: 2.25pt, right: 2.25pt, bottom: 6pt),
          align(left+horizon, text(weight: "bold", [Solución: ]) + body + align(right,block(inset: (right:3.75pt),$qed$)))
          )
        )
    
  • Why year is not an int?

      tag_year: "2025",
    

Some other stuff:

  • Where are images supposed to be used?
  • Why you need a filter, if you filter questions into different files?
  • If section only have one criterion and answer, why make separate functions instead of additional section parameters?
  • Why add name to banco.typ, if it’s already inside a named directory?

Here is a solution, that can be further significantly simplified, if you would mostly work with functions & data, and not content:

exam_year.typ
#import "preamble.typ": *

#show: title-book.with(
  title: "Ejercicios de exámenes",
  course: "Todas las comunidades",
  subject: "Por años",
  inst: none,
  show-answers: true,
  show-criteria: true,
  // filter: (year: 2024)
)

#include "ANDALUCÍA/andal_banco.typ"

// #include "ASTURIAS/astur_banco.typ"
preamble.typ
#let title-book(
  title: none,
  course: none,
  subject: none,
  inst: none,
  show-answers: false,
  show-criteria: false,
  filter: (:),
  body,
) = {
  set text(lang: "es")
  v(1.5fr)
  // block(width: 100%,
  //   align(horizon+center,text(hyphenate: false, weight: "bold", size: 40pt,
  //     upper(title))
  //   )
  // )
  align(horizon + center, {
    text(40pt, strong(upper(title)))
    v(1.75fr)
    set block(spacing: 1.2em)
    text(20pt, strong(course + " - " + subject))
    line(length: 50%)
    v(0.1fr)
    text(12pt, inst)
  })
  v(1fr)
  pagebreak(to: "odd")
  // pagebreak()
  outline()

  [#metadata(show-answers)<show-answers>]
  [#metadata(show-criteria)<show-criteria>]
  assert(type(filter) == dictionary)
  [#metadata(filter)<filter>]

  body
}

#let question(
  // scoring: 1,
  region: none,
  year: none,
  session: none,
  module: none,
  description: "Question description",
  ..sections,
) = context {
  let metadata = (
    region: region,
    year: year,
    session: session,
    module: module,
  )
  let filter = query(<filter>).last().value
  for (key, value) in filter {
    assert(key in metadata)
    if value != metadata.at(key) { return }
  }
  show: block.with(width: 100%, below: 1.6em)
  set enum(numbering: "a)")
  let (scorings, sections) = array.zip(..sections)
  let scoring = scorings.sum()
  let points = if scoring == 1 [punto] else [puntos]
  // box(width: 12.6pt,text(weight: "bold", enum(numbering: "1.")))
  h(12.6pt)
  [*(#scoring #points)*]
  description
  sections.join()
}

#let section(scoring: 1, body) = {
  let points = if scoring == 1 [punto] else [puntos]
  let body = {
    show: block.with(width: 100%)
    box(width: 100%, inset: (left: 16pt))[(#scoring #points) #body]
  }
  (scoring, body)
}

#let criteria(body) = context if query(<show-criteria>).last().value { body }

#let answer(body) = context {
  if not query(<show-answers>).last().value { return }
  show: rect.with(
    width: 100%,
    outset: (x: 2.25pt),
    inset: (top: 4pt, bottom: 6pt, rest: 0pt),
  )
  [*Solución:* #body]
  align(right, pad(right: 3.75pt, square(size: 6pt, fill: black)))
}
ANDALUCÍA/andal_banco.typ
#import "../preamble.typ": *


#question(
  // scoring: 1,
  region: "Andalucía",
  year: 2025,
  session: "Ordinaria",
  module: "Biomoléculas",
  description: [
    Los glóbulos rojos mantienen un contenido salino interno del 0,9 %, lo que es crucial para su función. Su membrana plasmática, compuesta en un alto porcentaje por fosfolípidos regula el equilibrio osmótico con el plasma sanguíneo. Considerando estos aspectos, razone:
  ],
  section(scoring: 0.5)[
    ¿Qué ocurriría con estas células si se inyectara a un individuo una solución salina que hiciera que la concentración final de sales en sangre fuese del 2,2 %?

    #criteria[
      0.25 por cada respuesta correcta.
    ]

    #answer[
      Los glóbulos rojos del organismo se encontrarían en un medio hipertónico y saldría agua de ellos (plasmólisis o crenación) con riesgo de muerte celular
    ]
  ],

  section(scoring: 0.5)[
    ¿Y si la concentración final de sales en sangre fuese del 0,01%?

    #criteria[
      0.25 por cada respuesta correcta.
    ]

    #answer[
      Los glóbulos rojos se encontrarían en un medio hipotónico y entraría agua en su interior, aumentando su volumen (turgencia) con el riesgo de estallido (citólisis)
    ]
  ],

  section(scoring: 1)[
    Indique la composición de los fosfolípidos y explique por qué su estructura los hace idóneos para formar membranas biológicas.

    #criteria[
      0.25 por cada respuesta correcta.
    ]

    #answer[
      Composición: una molécula de glicerina (glicerol), dos ácidos grasos y un ácido fosfórico unido a un aminoalcohol (alcohol) (0,4 puntos). Por su carácter anfipático las cabezas polares se orientan hacia el agua y las colas apolares hacia el lado opuesto, alejadas del agua, formando bicapas lipídicas (0,6 puntos)
    ]
  ],
)


Here is the simplest implementation:

questions_bank.typ
#import "ANDALUCÍA/banco.typ" as andal
// #import "ASTURIAS/banco.typ" as astur

#let questions-bank = (
  ..andal.questions,
)
exam_year.typ
#import "preamble.typ": questions, title-book
#import "questions_bank.typ": questions-bank

#show: title-book.with(
  title: "Ejercicios de exámenes",
  course: "Todas las comunidades",
  subject: "Por años",
  institute: "Institute",
)

#questions(
  show-answers: true,
  show-criteria: true,
  filter: (year: 2025),
  ..questions-bank,
)
preamble.typ
#let title-book(
  title: "Title",
  course: "Course",
  subject: "Subject",
  institute: none,
  body,
) = {
  set text(lang: "es")
  align(horizon + center, {
    set block(spacing: 1.2em)
    block(text(40pt, strong(upper(title))))
    v(17em)
    block(text(20pt)[*#course -- #subject*])
    line(length: 50%)
    if institute != none [#block(text(12pt, institute))]
  })
  pagebreak(to: "odd")
  body
}

/// Create a question.
#let question(
  region: none,
  year: none,
  session: none,
  module: none,
  description: "Question description",
  sections: (),
) = {
  let metadata = (
    region: region,
    year: year,
    session: session,
    module: module,
  )
  for (k, v) in metadata { assert(v != none, message: k + " is not set") }
  (metadata: metadata, description: description, sections: sections)
}

/// Create a section.
#let section(
  scoring: 1,
  problem: "Problem description",
  criterion: "Criterion",
  answer: "Answer",
) = (
  scoring: scoring,
  problem: problem,
  criterion: criterion,
  answer: answer,
)

/// Display a criterion.
#let display-criterion(body) = block(body)

/// Display an answer.
#let display-answer(body) = {
  show: rect.with(
    width: 100%,
    outset: (x: 2.25pt),
    radius: 3pt,
    inset: (top: 4pt, bottom: 6pt, rest: 0pt),
  )
  set block(spacing: 0pt)
  block[*Solución:* #body]
  align(right, pad(right: 3.75pt, square(size: 6pt, fill: black)))
}

/// Display a section.
#let display-section(
  scoring: 1,
  problem: "Problem description",
  criterion: "Criterion",
  answer: "Answer",
  show-answer: false,
  show-criterion: false,
) = {
  let points = if scoring == 1 [punto] else [puntos]
  show: enum.item
  [(#scoring #points) #problem]
  if show-criterion { display-criterion(criterion) }
  if show-answer { display-answer(answer) }
}


/// Display filtered questions.
#let display-question(
  show-answers: false,
  show-criteria: false,
  filter: (:),
  question,
) = {
  let (sections, description) = question
  let max-score = sections.map(s => s.scoring).sum()
  let points = if max-score == 1 [punto] else [puntos]
  show: enum.item
  set block(below: 1.6em)
  set enum(numbering: "a)")
  [*(#max-score #points)* #description]
  for section in sections {
    display-section(
      show-answer: show-answers,
      show-criterion: show-criteria,
      ..section,
    )
  }
}

/// Display filtered questions.
#let questions(
  show-answers: false,
  show-criteria: false,
  filter: (:),
  ..questions,
) = {
  assert(type(filter) == dictionary)
  questions = questions
    .pos()
    .filter(q => filter.pairs().all(((k, v)) => q.metadata.at(k) == v))
  for question in questions {
    display-question(
      show-answers: show-answers,
      show-criteria: show-criteria,
      question,
    )
  }
}
ANDALUCÍA/banco.typ
#import "../preamble.typ": *
#let questions = (
  question(
    region: "Andalucía",
    year: 2025,
    session: "Ordinaria",
    module: "Biomoléculas",
    description: [
      Los glóbulos rojos mantienen un contenido salino interno del 0,9 %, lo que
      es crucial para su función. Su membrana plasmática, compuesta en un alto
      porcentaje por fosfolípidos regula el equilibrio osmótico con el plasma
      sanguíneo. Considerando estos aspectos, razone:
    ],
    sections: (
      section(
        scoring: 0.5,
        problem: "¿Qué ocurriría con estas células si se inyectara a un individuo una solución salina que hiciera que la concentración final de sales en sangre fuese del 2,2 %?",
        criterion: "0.25 por cada respuesta correcta.",
        answer: "Los glóbulos rojos del organismo se encontrarían en un medio hipertónico y saldría agua de ellos (plasmólisis o crenación) con riesgo de muerte celular",
      ),
      section(
        scoring: 0.5,
        problem: "¿Y si la concentración final de sales en sangre fuese del 0,01%?",
        criterion: "0.25 por cada respuesta correcta.",
        answer: "Los glóbulos rojos se encontrarían en un medio hipotónico y entraría agua en su interior, aumentando su volumen (turgencia) con el riesgo de estallido (citólisis)",
      ),
      section(
        scoring: 1,
        problem: "Indique la composición de los fosfolípidos y explique por qué su estructura los hace idóneos para formar membranas biológicas.",
        criterion: "0.25 por cada respuesta correcta.",
        answer: "Composición: una molécula de glicerina (glicerol), dos ácidos grasos y un ácido fosfórico unido a un aminoalcohol (alcohol) (0,4 puntos). Por su carácter anfipático las cabezas polares se orientan hacia el agua y las colas apolares hacia el lado opuesto, alejadas del agua, formando bicapas lipídicas (0,6 puntos)",
      ),
    ),
  ),
)

No context is used, and filtering is only done once, so it’s as performant as it gets. The rest is just styling of preprocessed data.

You can skip the structure functions and just use raw dictionaries, but it might introduce some other issues.

1 Like

First of all, sorry for the delay in replying — the last two weeks of school are always chaotic.

Secondly, thank you so much, this is basically exactly what I was looking for. I’m going to try to respond to each of the things you mentioned:

Each file contains questions from a specific region, and the idea was to import them into a single document that would group all the questions together.

That’s meant to show the name of the school where I work, but I’ll probably change it to display something else.

I just didn’t notice — I know it should be lowercase, but I didn’t get an error.

Honestly, I’m not really sure.

I based this on an example I found and modified it from there. I didn’t know you could do it that way.

I wanted the content to always start on an odd page. From what you showed me, it looks like it’s possible to handle that directly.

My intention is to include it at some point. In the future, I’ll add other files similar to exam_year.typ organized by region, topic, or other categories — for example, something like this:

= Andalucía
#questions(
  show-answers: true,
  show-criteria: true,
  filter: (region: "Andalucía"),
  ..questions-bank,
)

= Asturias
#questions(
  show-answers: true,
  show-criteria: true,
  filter: (region: "Asturias"),
  ..questions-bank,
)

Some questions may not have sections — just a main prompt. So I’d like to be able to set a score manually when needed.

No real reason… I guess it’s just a habit, haha.

Originally I was planning to separate the exercise number from the prompt, and I wanted it to always take up the same amount of space regardless of the number.

I was trying different styles. I didn’t know that you could adjust weight using strong.

They got deleted and I didn’t notice, honestly.

I took that from another forum post and thought I might use it. But you’re right — I don’t need a filter in each question bank individually, just in the document that combines them all.

This is true — each section should have a single criterion and answer, so it makes more sense to include them as parameters within section.

I plan to keep the documents locally, and I often use the file search feature instead of navigating through folders.

Thanks again, this is exactly what I needed.

One more question: would it be possible to select just some specific sections from a question that has multiple sections?

I suppose I’d need to modify the question function to allow filtering a specific question:

/// Create a question.
#let question(
  region: none,
  year: none,
  session: none,
  module: none,
  position: none,
  description: "Question description",
  sections: (),
) = {
  let metadata = (
    region: region,
    year: year,
    session: session,
    module: module,
  )
  for (k, v) in metadata { assert(v != none, message: k + " is not set") }
  (metadata: metadata, description: description, sections: sections)
}

And, of course, I guess I’d need to do something similar for section:

/// Create a section.
#let section(
  scoring: 1,
  position: none,
  problem: "Problem description",
  criterion: "Criterion",
  answer: "Answer",
) = (
  scoring: scoring,
  problem: problem,
  criterion: criterion,
  answer: answer,
)

/// Display a section.
#let display-section(
  scoring: 1,
  position: none,
  problem: "Problem description",
  criterion: "Criterion",
  answer: "Answer",
  show-answer: false,
  show-criterion: false,
) = {
  let points = if scoring == 1 [point] else [points]
  show: enum.item
  [(#scoring #points) #problem]
  if show-criterion { display-criterion(criterion) }
  if show-answer { display-answer(answer) }
}

Maybe I’d need to add another parameter to question to indicate that I want only section 1, or 1–3, or all of them?

1 Like

Just importing everything from every file doesn’t actually do anything. You need to insert the imported stuff or use include.

The docs only ever use lowercase. Do you use uppercase somewhere else, or just chose randomly? Document that `text.lang` is case-insensitive, or make it lowercase-only · Issue #6549 · typst/typst · GitHub

The documentation shows line.length right from the start.

I assumed there are always sections, hence the need for automatic sum.

Well, - is easier to type, and readable names are (almost) always better.

The documentation for this field has a dedicated paragraph about strong.

Usually (for me) any (fuzzy) file search is path-based, so it will match directory and file name, so renaming files is redundant.

I don’t know which interface you want. But you can just check section positions.

/// Create a question.
#let question(
  region: none,
  year: none,
  session: none,
  module: none,
  description: "Question description",
  show-sections: auto,
  sections: (),
) = {
  let metadata = (
    region: region,
    year: year,
    session: session,
    module: module,
  )
  for (k, v) in metadata { assert(v != none, message: k + " is not set") }
  (
    metadata: metadata,
    description: description,
    show-sections: show-sections,
    sections: sections,
  )
}

/// Display filtered questions.
#let display-question(
  show-answers: false,
  show-criteria: false,
  filter: (:),
  question,
) = {
  let (sections, show-sections, description) = question
  let max-score = sections.map(s => s.scoring).sum()
  let points = if max-score == 1 [punto] else [puntos]
  show: enum.item
  set block(below: 1.6em)
  set enum(numbering: "a)")
  [*(#max-score #points)* #description]
  for (i, section) in sections.enumerate() {
    if show-sections != auto and (i + 1) not in show-sections { continue }
    display-section(
      show-answer: show-answers,
      show-criterion: show-criteria,
      ..section,
    )
  }
}

#let questions = (
  question(
    region: "Andalucía",
    year: 2025,
    session: "Ordinaria",
    module: "Biomoléculas",
    description: [
      Los glóbulos rojos mantienen un contenido salino interno del 0,9 %, lo que
      es crucial para su función. Su membrana plasmática, compuesta en un alto
      porcentaje por fosfolípidos regula el equilibrio osmótico con el plasma
      sanguíneo. Considerando estos aspectos, razone:
    ],
    // show-sections: (2, 3),
    // show-sections: range(4),
    show-sections: range(1, 3),
    sections: (),
  )
)

I’ve modified it so that it works in such a way that if there are no sections, it takes the scoring value from the question. Additionally, it allows adding the criterion and the answer. Let me know what you think.

/// Create a question.
#let question(
  region: none,
  year: none,
  session: none,
  module: none,
  scoring: none,
  description: "Question description",
  criterion: "Criterion",
  answer: "Answer",
  sections: (),
) = {
  let metadata = (
    region: region,
    year: year,
    session: session,
    module: module,
  )
  for (k, v) in metadata { assert(v != none, message: k + " is not set") }
  (metadata: metadata, description: description, sections: sections, scoring: scoring, criterion: criterion, answer: answer)
}

/// Create a section.
#let section(
  scoring: 1,
  problem: "Problem description",
  criterion: "Criterion",
  answer: "Answer",
) = (
  scoring: scoring,
  problem: problem,
  criterion: criterion,
  answer: answer,
)

/// Display a criterion.
#let display-criterion(body) = block(body)

/// Display an answer.
#let display-answer(body) = {
  show: rect.with(
    width: 100%,
    outset: (x: 2.25pt),
    radius: 3pt,
    inset: (top: 4pt, bottom: 6pt, rest: 0pt),
  )
  block(inset: (top: 2pt, left: 2pt, right: 2pt), width:100%, [*Solución:* #body])
  align(right, pad(right: 3.75pt, square(size: 6pt, fill: black)))
}

/// Display a section.
#let display-section(
  scoring: 1,
  problem: "Problem description",
  criterion: "Criterion",
  answer: "Answer",
  show-answer: false,
  show-criterion: false,
) = {
  let points = if scoring == 1 [punto] else [puntos]
  show: enum.item
  [#box[(#scoring #points)] #problem]
  if show-criterion { display-criterion(criterion) }
  if show-answer { display-answer(answer) }
}

/// Display filtered questions.
#let display-question(
  scoring: none,
  show-answers: false,
  show-criteria: false,
  filter: (:),
  question,
) = {
  let (sections, description, scoring, criterion, answer) = question
  let max-score = { if sections == () {scoring} else {calc.round(sections.map(s => s.scoring).sum())}}
  let points = if max-score == 1 [punto] else [puntos]
  show: enum.item
  set block(below: 1.6em)
  set enum(numbering: "a)")
  [#box[*(#max-score #points)*] #description]
  for section in sections {
    display-section(
      show-answer: show-answers,
      show-criterion: show-criteria,
      ..section,
    )
  }
  if sections == () {if show-criteria { display-criterion(criterion) }
  if show-answers { display-answer(answer) }}
}

/// Display filtered questions.
#let questions(
  show-answers: false,
  show-criteria: false,
  filter: (:),
  ..questions,
) = {
  assert(type(filter) == dictionary)
  questions = questions
    .pos()
    .filter(q => filter.pairs().all(((k, v)) => q.metadata.at(k) == v))
  for question in questions {
    display-question(
      show-answers: show-answers,
      show-criteria: show-criteria,
      question,
    )
  }
}

I was referring to being able to filter within sections as well, so that each section has an identifier and, when displaying a question, I can choose which section to show. I have the following question:

#question(
  region: "Andalucía",
  year: 2024,
  session: "Ordinaria",
  module: "Álgebra",
  identifier-question: 17,
  description: [
     Solve the following equations:
  ],
  sections: (
    section(
      identifier-section: 1
      scoring: 0.5,
      problem: [
        $x^2-3x+1=0$
      ],
    ),
    section(
      identifier-section: 2
      scoring: 1.5,
      problem: [
        $x^2-4=0$
      ],
    ),
    section(
      identifier-section: 3
      scoring: 0.75,
      problem: [
        $x^2-x=0$
      ],
    ),
    section(
      identifier-section: 4
      scoring: 0.75,
      problem: [
        $x^2-x=0$
      ],
    ),
    section(
      identifier-section: 5
      scoring: 0.75,
      problem: [
        $x^2-x=0$
      ],
    ),
  ),
),

And then be able to request, for example, to display question 1 with sections 1, 3, 4, 5.

#questions(
  show-answers: show-answers,
  show-criteria: show-criteria,
  filter: (region: "Andalucía", year: 2024, session: "Ordinaria", module: "Álgebra", identifier-question: 17, identifier-sections: (1,3,4,5),
  ..questions-bank,
)

Initially, I saw this as a quick way to export a set of questions with a specific typology (year, region, etc.). Now I also see the possibility of using it to generate my exams by adding an identifier to each question and each section. However, for this, I would need to write the previous code

for every question I want to include, and also keep in mind that the scoring varies depending on the sections I choose.