diff --git a/models/report_correction_model/README.md b/models/report_correction_model/README.md new file mode 100644 index 0000000..6fa58c1 --- /dev/null +++ b/models/report_correction_model/README.md @@ -0,0 +1,133 @@ +# Grading Automation Tools + +This project provides a set of Python scripts to automate and standardize the process of creating grading reports for student assignments. The solution is divided into two main tools: + +1. **Report Generator (`report_tool.py`)**: Creates a folder and file structure for grading in Markdown (`.md`) for each student from a customizable template. +2. **PDF Converter (`convert_to_pdf.py`)**: Converts the finalized Markdown grading reports to PDF, preserving formatting, tables, and mathematical formulas in LaTeX. + +## ✨ Features + + - **Batch Generation**: Create the grading structure for all students in a class at once. + - **Standardized Template**: Ensure consistency across all grades by using a template file (`template.md`). + - **Customizable**: Easily modify the `template.md` to adapt to different evaluation criteria. + - **High-Quality PDF Conversion**: Generate professional-looking PDFs by rendering Markdown with a style similar to GitHub. + - **LaTeX Support**: Write complex mathematical formulas in your reports using LaTeX syntax, which will be correctly rendered in the final PDF. + +## 📂 Expected Directory Structure + +For the scripts to work correctly, organize your folders as follows: + +``` +your_project/ +├── submissions/ <-- Folder with each student's directory +│ ├── Example_1/ +│ ├── Example_2/ +│ └── Example_3/ +├── report_tool.py <-- Generator script +├── convert_to_pdf.py <-- Converter script +└── template.md <-- Report template +``` + + - The main directory (e.g., `submissions/`) should contain a subfolder for each student. The name of the subfolder will be used as the student's name in the report. + +## 🚀 Installation and Setup + +### Prerequisites + + - Python 3.7 or higher. + +### Installation Steps + +1. **Clone or download the files** to a directory on your computer. + +2. **Create a virtual environment (recommended)**: + + ```bash + python -m venv venv + ``` + + - On Windows, activate with: `.\venv\Scripts\activate` + - On macOS/Linux, activate with: `source venv/bin/activate` + +3. **Install the necessary dependencies**: + The scripts use a few Python libraries to render Markdown and generate the PDF. Install them with the following command: + + ```bash + pip install "markdown-it-py<3.0.0" "mdit-py-plugins" pyppeteer + ``` + + > **Note**: The first time the `convert_to_pdf.py` script is run, the `pyppeteer` library will automatically download a version of Chromium (a headless browser) to render the page. This may take a few minutes. + +## 📝 Workflow and Usage + +### Step 1: Generate the Grading Files + +Run the `report_tool.py` script to create the `.md` files for each student. + +**Basic Usage:** +Provide the activity name as the main argument. The script will look for student folders in the current directory (`.`). + +```bash +# Example for an activity named "Lab01" +python report_tool.py Lab01 +``` + +**Advanced Usage with Options:** +You can specify the student directory, the grader's name, and the report title. + +```bash +python report_tool.py "Lab02_Circuits" -o "./submissions" -g "Prof. Ada Lovelace" --activity-title "Laboratory 02 Report" +``` + +After running, the following structure will be created inside each student's folder: + +``` +submissions/ +└── John_Smith/ + └── John_Smith_Lab01_correction/ + ├── John_Smith_Lab01_correction.md <-- The report to be filled out + └── src/ <-- Optional folder for corrected code +``` + +### Step 2: Fill in the Reports + +Open each generated `.md` file and fill in the grades, comments, and feedback for each student. + +### Step 3: Convert the Reports to PDF + +Once the grades are finalized, run the `convert_to_pdf.py` script to generate the PDFs. + +**Usage:** +Provide the directory where the `.md` reports are located. The script will recursively search for all `.md` files and convert them. + +```bash +# Converts all reports inside the 'submissions' folder +python convert_to_pdf.py ./submissions +``` + +The script will create a `.pdf` file with the same name and in the same location as the original `.md` file. + +## 🔧 Component Details + +### `report_tool.py` + + - **Purpose**: To automate the creation of the grading structure. + - **Command-Line Arguments**: + - `activity_name` (required): A short name for the activity, used for naming files. + - `-o, --output-dir`: The directory where the student folders are located (default: `.`). + - `-g, --grader`: The name of the grader to be inserted into the report. + - `-t, --activity-title`: The full title of the activity for the header. If omitted, it uses `activity_name`. + - `--template`: The path to the template file (default: `template.md`). + +### `convert_to_pdf.py` + + - **Purpose**: To convert Markdown files to PDF. + - **Technology**: Uses `pyppeteer` to control a headless browser that "prints" the HTML version of the Markdown to a PDF file. + - **Command-Line Arguments**: + - `base_dir` (optional): The base directory to recursively search for `.md` files (default: `.`). + +### `template.md` + + - **Purpose**: To serve as a model for all grading reports. + - **Syntax**: Uses the `${variable}` syntax from Python's `string.Template` for fields that will be replaced by `report_tool.py`, such as `${student_name}`, `${activity_title}`, etc.. + - **Customization**: Feel free to edit this file, changing the evaluation criteria, weights, text, and the calculation formula to meet your needs. Just keep the placeholders that the script uses. diff --git a/models/report_correction_model/convert_to_pdf.py b/models/report_correction_model/convert_to_pdf.py new file mode 100644 index 0000000..f40843b --- /dev/null +++ b/models/report_correction_model/convert_to_pdf.py @@ -0,0 +1,150 @@ +import argparse +import asyncio +import sys +from pathlib import Path + +from markdown_it import MarkdownIt +from mdit_py_plugins.front_matter import front_matter_plugin +from mdit_py_plugins.texmath import texmath_plugin + +from pyppeteer import launch + +# --- HTML TEMPLATE --- +HTML_TEMPLATE = """ + + + + + {title} + + + + + + {content} + + +""" + +# --- HELPER FUNCTION --- +def log(msg: str, success: bool = True, indent: int = 0) -> None: + GREEN, RED, YELLOW, RESET = "\033[92m", "\033[91m", "\033[93m", "\033[0m" + prefix = f"{GREEN}✅{RESET}" if success else (f"{YELLOW}ℹ️{RESET}" if success is None else f"{RED}❌{RESET}") + print(f"{' ' * indent}{prefix} {msg}") + +# --- CORE LOGIC --- +async def convert_file_to_pdf(md_path: Path, browser): + pdf_path = md_path.with_suffix(".pdf") + log(f"Processando '{md_path.name}'...", success=None, indent=1) + + try: + # Plugin de matemática sem argumento extra + md_converter = ( + MarkdownIt("gfm-like") + .use(front_matter_plugin) + .use(texmath_plugin) # ✅ sem argumento + ) + + markdown_text = md_path.read_text(encoding="utf-8") + html_content = md_converter.render(markdown_text) + final_html = HTML_TEMPLATE.format(title=md_path.stem, content=html_content) + + page = await browser.newPage() + await page.setContent(final_html) + await page.pdf({ + "path": str(pdf_path), + "format": "A4", + "printBackground": True, + "margin": {"top": "2.5cm", "right": "2.5cm", "bottom": "2.5cm", "left": "2.5cm"}, + }) + await page.close() + + log(f"Convertido com sucesso para '{pdf_path.name}'", success=True, indent=2) + return True + + except Exception as e: + log(f"Falha na conversão de '{md_path.name}'", success=False, indent=2) + log(f"Erro: {e}", success=False, indent=3) + return False + +async def main_converter(base_dir: Path): + print("\n" + "="*50) + print("🚀 Iniciando Conversão de Markdown para PDF") + print(f"📂 Diretório de Busca: {base_dir.resolve()}") + print("="*50 + "\n") + + markdown_files = list(base_dir.rglob("*.md")) + if not markdown_files: + log("Nenhum arquivo Markdown (.md) foi encontrado para conversão.", success=None) + print("\n✨ Processo concluído!") + return + + log(f"Encontrados {len(markdown_files)} arquivos Markdown para processar.") + log("Iniciando o navegador headless (pode demorar na primeira vez)...", success=None) + + browser = None + try: + browser = await launch(headless=True, args=['--no-sandbox']) + + success_count = 0 + fail_count = 0 + + tasks = [convert_file_to_pdf(md_path, browser) for md_path in markdown_files] + results = await asyncio.gather(*tasks) + + for result in results: + if result: + success_count += 1 + else: + fail_count += 1 + + except Exception as e: + log("Ocorreu um erro ao iniciar o navegador ou processar os arquivos.", success=False) + log(f"Erro: {e}", success=False) + + finally: + if browser: + await browser.close() + log("Navegador headless fechado.", success=None) + + print("\n" + "="*50) + print("✨ Processo de conversão concluído!") + log(f"Sucessos: {success_count}", success=True) + log(f"Falhas: {fail_count}", success=False) + print("="*50) + + +# --- SCRIPT ENTRY POINT --- +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Ferramenta para converter arquivos Markdown (.md) em PDF usando Pyppeteer.", + formatter_class=argparse.HelpFormatter + ) + parser.add_argument( + "base_dir", + type=Path, + nargs='?', + default=Path("."), + help="Diretório base para buscar arquivos .md recursivamente. Padrão: diretório atual." + ) + args = parser.parse_args() + + if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + asyncio.run(main_converter(args.base_dir)) diff --git a/models/report_correction_model/report_tool.py b/models/report_correction_model/report_tool.py new file mode 100644 index 0000000..67b9f96 --- /dev/null +++ b/models/report_correction_model/report_tool.py @@ -0,0 +1,159 @@ +import argparse +import sys +import re +from datetime import datetime +from string import Template +from pathlib import Path +from typing import Set + +# --- CONSTANTS --- +# Pastas a serem ignoradas durante a busca por diretórios de alunos. +IGNORED_FOLDERS: Set[str] = {"__pycache__", ".git", ".idea", ".vscode"} + + +# --- HELPER FUNCTIONS --- +def safe_filename(name: str) -> str: + """Remove caracteres inválidos para criar um nome de arquivo seguro.""" + # Remove qualquer caractere que não seja letra, número, sublinhado ou hífen. + return re.sub(r'[^\w-]', '_', name) + + +def log(msg: str, success: bool = True) -> None: + """Imprime mensagens de log com formatação colorida (sucesso/erro).""" + # Códigos de escape ANSI para cores no terminal + GREEN = "\033[92m" + RED = "\033[91m" + RESET = "\033[0m" + + prefix = f"{GREEN}✅{RESET}" if success else f"{RED}❌{RESET}" + print(f"{prefix} {msg}") + + +def load_template(template_path: Path) -> Template: + """Carrega o conteúdo do arquivo de template especificado.""" + try: + content = template_path.read_text(encoding="utf-8") + return Template(content) + except FileNotFoundError: + log(f"Arquivo de template não encontrado em: {template_path}", success=False) + sys.exit(1) # Encerra o script se o template não existir. + except IOError as e: + log(f"Erro ao ler o arquivo de template: {e}", success=False) + sys.exit(1) + + +# --- CORE LOGIC --- +def create_correction_files(args: argparse.Namespace) -> None: + """Função principal que itera sobre as pastas e cria os arquivos de correção.""" + base_dir = args.output_dir.resolve() # .resolve() obtém o caminho absoluto + md_template = load_template(args.template) + + print("\n" + "="*50) + print("🚀 Iniciando a Geração de Relatórios de Correção") + print(f"📂 Diretório Base: {base_dir}") + print(f"📄 Atividade: {args.activity_name}") + print(f"🧑‍🏫 Corretor: {args.grader}") + print("="*50 + "\n") + + if not base_dir.is_dir(): + log(f"O diretório base '{base_dir}' não existe ou não é um diretório.", success=False) + return + + student_dirs_found = 0 + for item in base_dir.iterdir(): + # Pula arquivos e pastas ignoradas + if not item.is_dir() or item.name in IGNORED_FOLDERS: + continue + + student_dirs_found += 1 + student_name = item.name + safe_name = safe_filename(student_name) + + base_name = f"{safe_name}_{args.activity_name}_correction" + correction_folder = item / base_name + md_file_path = correction_folder / f"{base_name}.md" + src_folder_path = correction_folder / "src" + + try: + # Cria as pastas de correção e 'src' (exist_ok=True evita erro se já existirem) + src_folder_path.mkdir(parents=True, exist_ok=True) + + # Preenche as variáveis do template + content_to_write = md_template.substitute( + activity_title=args.activity_title, + grader_name=args.grader, + student_name=student_name, + correction_date=datetime.now().strftime("%Y-%m-%d"), + final=args.default_grade, + ) + + # Escreve o arquivo Markdown + md_file_path.write_text(content_to_write, encoding="utf-8") + log(f"Estrutura de correção criada para '{student_name}' em: {correction_folder}") + + except OSError as e: + log(f"Erro ao criar estrutura para '{student_name}': {e}", success=False) + + if student_dirs_found == 0: + log("Nenhum diretório de aluno encontrado para processar.", success=False) + + print("\n" + "="*50) + print("✨ Processo concluído!") + print("="*50) + + +# --- SCRIPT ENTRY POINT --- +def main(): + """Analisa os argumentos da linha de comando e inicia o script.""" + parser = argparse.ArgumentParser( + description="Ferramenta para criar estruturas de correção para atividades de alunos.", + formatter_class=argparse.HelpFormatter + ) + + parser.add_argument( + "activity_name", + type=str, + help="Nome ou número da atividade (ex: 'Exp01', 'Lab_AND_Gate'). Usado nos nomes dos arquivos." + ) + parser.add_argument( + "-t", "--activity-title", + type=str, + help="Título completo da atividade para o cabeçalho do relatório. Se não for fornecido, usa o 'activity_name'.", + default=None + ) + parser.add_argument( + "-o", "--output-dir", + type=Path, + default=Path("."), + help="Diretório base contendo as pastas dos alunos. Padrão: diretório atual." + ) + parser.add_argument( + "-g", "--grader", + type=str, + default="Nome do Corretor", + help="Nome do corretor a ser inserido no relatório." + ) + parser.add_argument( + "-d", "--default-grade", + type=str, + default="[INSERIR NOTA]", + help="Valor padrão para o campo da nota final." + ) + parser.add_argument( + "--template", + type=Path, + default=Path("template.md"), + help="Caminho para o arquivo de template Markdown. Padrão: 'template.md' no mesmo diretório." + ) + + args = parser.parse_args() + + # Se o título não for fornecido, usa o nome da atividade como padrão. + if args.activity_title is None: + args.activity_title = args.activity_name.replace('_', ' ').title() + + create_correction_files(args) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/models/report_correction_model/template.md b/models/report_correction_model/template.md new file mode 100644 index 0000000..3ed86b4 --- /dev/null +++ b/models/report_correction_model/template.md @@ -0,0 +1,44 @@ +# Relatório de Correção: ${activity_title} + +| **Corretor:** | ${grader_name} | +| :--- | :--- | +| **Aluno(a):** | ${student_name} | +| **Matrícula:** | [Inserir Matrícula] | +| **Data da Correção:** | ${correction_date} | + +--- + +## 📝 Resumo da Avaliação + +| Nota Final | +| :---: | +| **${final}** | + +### Comentários Gerais +[Forneça um parágrafo de resumo aqui sobre os pontos fortes e as áreas para melhoria.] + +--- + +## 📊 Detalhamento da Nota + +| Critério | Seção do Relatório | Peso | Nota (0-10) | Comentários Específicos | +| :--- |:---|:---:|:---:|:---| +| 📄 **Documentação** | Introdução, Teoria, Código, Análise e Conclusão | 10% | `[Nota Doc]` | [Feedback sobre a qualidade da escrita, clareza e formatação.] | +| ⚙️ **Compilação** | Compilação | 20% | `[Nota Comp]` | [Feedback sobre a compilação do código, ausência de warnings, etc.] | +| 🚀 **Simulação** | Simulação, Análise e Resultados | 70% | `[Nota Sim]` | [Feedback sobre a corretude da simulação, formas de onda e análise dos resultados.] | +| 🕒 **Fator Pontualidade** | - | - | `[Fator Pontualidade]` | [Ex: 1.0 (dentro do prazo), 0.8 (atraso de 1 dia)] | + +### Fórmula de Cálculo + +A nota final é calculada usando a seguinte fórmula: + +$$ +\text{Nota}_{\text{final}} = \Big( (N_{Doc} \cdot 0.1) + (N_{Comp} \cdot 0.2) + (N_{Sim} \cdot 0.7) \Big) \cdot F_{\text{Pontualidade}} +$$ + +**Cálculo prático com suas notas:** + +`= ( ( [Nota Doc] * 0.1 ) + ( [Nota Comp] * 0.2 ) + ( [Nota Sim] * 0.7 ) ) * [Fator Pontualidade]` + +--- +**[Comentários finais ou de encerramento]**