diff --git a/json-assignment-validator/ReadME.md b/json-assignment-validator/ReadME.md new file mode 100644 index 00000000..1796691d --- /dev/null +++ b/json-assignment-validator/ReadME.md @@ -0,0 +1,190 @@ +# 📝 TS Marking Script (R-U-Certified) + +A Python-based JSON validation tool used to assess Isomer training assignment submissions. + +The script validates structured page JSON files against predefined rules and generates a CSV report summarising results. + +--- + +## 🚀 Features + +* Validates JSON assignment submissions +* Displays PASS / FAIL results in console +* Generates timestamped CSV report +* Uses only Python standard library (no external dependencies) + +--- + +## 📦 Project Structure + +```bash +json-assignment-validator/ +├── marking.py +├── assignment-submission/ +├── test-examples/ +└── reports/ +``` + +* `marking.py` — Main validation script +* `assignment-submission/` — Folder containing student JSON files +* `test-examples/` — Sample JSON files (pass/fail examples) +* `reports/` — Auto-generated CSV reports + +--- + +## 🧰 Requirements + +* Python 3.8+ +* Visual Studio Code (recommended) + +Verify Python installation: + +```bash +python3 --version +``` + +--- + +## 📁 Setup + +### 1️⃣ Clone the Repository + +Using GitHub Desktop or Git: + +```bash +git clone +``` + +Open the project folder in VS Code. + +--- + +### 2️⃣ Create Submission Folder + +Inside the project directory, ensure this folder exists: + +```bash +assignment-submission/ +``` + +--- + +### 3️⃣ Add Student Submissions + +* Export student JSON files from **Isomer Studio Sandbox**. +* Place the exported `.json` files inside `assignment-submission/`. + +--- + +## ▶️ Running the Script + +Open the VS Code terminal and run: + +```bash +python3 marking.py assignment-submission +``` + +If `python3` does not work, try: + +```bash +python marking.py assignment-submission +``` + +--- + +## 📊 Output + +### Console Output + +Example: + +```bash +✅ PASS: student1.json (Title: My Page) +❌ FAIL: student2.json (Title: Home Page) + - Summary cannot be the default text 'This is the page summary' + - Missing ordered list +``` + +Summary: + +```bash +Total files: 3 +Passed: 1 +Failed: 2 +``` + +--- + +### 📄 CSV Report + +A timestamped CSV file is generated in the `reports/` folder: + +``` +reports/_report_YYYYMMDD_HHMMSS.csv +``` + +Columns: + +| Name | Status | Errors | +| ---- | ------ | ------ | + +* **Name** → Extracted from page title +* **Status** → PASS or FAIL +* **Errors** → Listed per validation failure + +--- + +## ✅ Validation Rules + +The script checks: + +### Page Summary + +* Cannot be empty +* Cannot be "This is the page summary" + +### Content Structure + +* At least 2 prose blocks +* One unordered list +* One ordered list +* Nested sub-lists required + +### Headings + +* First heading must be level 2 +* Cannot skip heading levels + +### Tables + +* Minimum 1 table +* Caption required (not default text) +* Must contain headers and cells + +### Accordions + +* Exactly 2 required +* Each must have a summary + +### Images + +* Minimum 1 image +* Alt text required +* No placeholder alt text +* No placeholder image source + +### Infocards + +* Component must exist +* Each card must have title, image alt text, and image URL + +--- + +## 🆘 Troubleshooting + +* Ensure all files end with `.json` +* Read error messages carefully — they indicate what needs fixing +* Verify Python is installed correctly + +--- + diff --git a/json-assignment-validator/marking.py b/json-assignment-validator/marking.py new file mode 100644 index 00000000..38de59ac --- /dev/null +++ b/json-assignment-validator/marking.py @@ -0,0 +1,476 @@ +# marking.py +import json +import os +import sys +import csv +from pathlib import Path +from datetime import datetime + +def find_content_items(content, item_type): + """Recursively find all items of a specific type in content""" + items = [] + + if isinstance(content, dict): + if content.get('type') == item_type: + items.append(content) + for value in content.values(): + items.extend(find_content_items(value, item_type)) + elif isinstance(content, list): + for item in content: + items.extend(find_content_items(item, item_type)) + + return items + +def validate_heading_hierarchy(content): + """Validate that headings start at level 2 and follow hierarchy across entire document""" + errors = [] + headings = find_content_items(content, 'heading') + + if not headings: + return [] # No headings found, skip validation + + # Check if first heading is level 2 + if headings[0].get('attrs', {}).get('level') != 2: + errors.append("First heading must be level 2") + + # Check hierarchy - each heading should not skip levels + for i in range(len(headings) - 1): + current_level = headings[i].get('attrs', {}).get('level', 0) + next_level = headings[i + 1].get('attrs', {}).get('level', 0) + + # Next heading can be same level, one level deeper, or any level shallower + if next_level > current_level + 1: + errors.append(f"Heading hierarchy error: jumped from level {current_level} to level {next_level}") + + return errors + +def validate_prose_blocks_with_lists(content): + """Validate at least 2 prose blocks, one with unordered list and one with ordered list""" + errors = [] + + if not isinstance(content, list): + return errors + + # Find all prose blocks + prose_blocks = [item for item in content if isinstance(item, dict) and item.get('type') == 'prose'] + + if len(prose_blocks) < 2: + errors.append(f"Need at least 2 prose blocks (found: {len(prose_blocks)})") + return errors + + # Check for unordered list in at least one prose block + has_unordered_in_prose = False + for prose in prose_blocks: + prose_content = prose.get('content', []) + if find_content_items(prose_content, 'unorderedList'): + has_unordered_in_prose = True + break + + if not has_unordered_in_prose: + errors.append("At least one prose block must contain an unordered list") + + # Check for ordered list in at least one prose block + has_ordered_in_prose = False + for prose in prose_blocks: + prose_content = prose.get('content', []) + if find_content_items(prose_content, 'orderedList'): + has_ordered_in_prose = True + break + + if not has_ordered_in_prose: + errors.append("At least one prose block must contain an ordered list") + + return errors + +def validate_nested_lists(content): + """Validate that lists have nested sub-lists""" + errors = [] + + # Check unordered lists + unordered_lists = find_content_items(content, 'unorderedList') + has_nested_unordered = False + for ul in unordered_lists: + for item in ul.get('content', []): + if isinstance(item, dict) and item.get('type') == 'listItem': + # Check if this list item contains a nested list + item_content = item.get('content', []) + if find_content_items(item_content, 'unorderedList'): + has_nested_unordered = True + break + if has_nested_unordered: + break + + if not has_nested_unordered: + errors.append("Unordered list must have nested sub-lists") + + # Check ordered lists + ordered_lists = find_content_items(content, 'orderedList') + has_nested_ordered = False + for ol in ordered_lists: + for item in ol.get('content', []): + if isinstance(item, dict) and item.get('type') == 'listItem': + # Check if this list item contains a nested list + item_content = item.get('content', []) + if find_content_items(item_content, 'orderedList'): + has_nested_ordered = True + break + if has_nested_ordered: + break + + if not has_nested_ordered: + errors.append("Ordered list must have nested sub-lists") + + return errors + +def validate_table_structure_and_caption(content): + """Validate that tables have headers, cells, and proper captions""" + errors = [] + tables = find_content_items(content, 'table') + + if not tables: + errors.append("Missing table (at least 1 required)") + return errors + + for table_idx, table in enumerate(tables, 1): + # Validate caption + caption = table.get('attrs', {}).get('caption', '') + + if not caption or caption.strip() == '': + errors.append(f"Table {table_idx}: Caption cannot be empty") + elif caption == "Table caption": + errors.append(f"Table {table_idx}: Caption cannot be the default text 'Table caption'") + + # Validate table structure (has headers and cells) + has_header = False + has_cell = False + + table_content = table.get('content', []) + for row in table_content: + if isinstance(row, dict) and row.get('type') == 'tableRow': + row_content = row.get('content', []) + for cell in row_content: + if isinstance(cell, dict): + if cell.get('type') == 'tableHeader': + has_header = True + elif cell.get('type') == 'tableCell': + has_cell = True + + if not has_header: + errors.append(f"Table {table_idx}: Missing table headers (tableHeader)") + if not has_cell: + errors.append(f"Table {table_idx}: Missing table cells (tableCell)") + + return errors + +def validate_accordions(content): + """Validate that exactly 2 accordions are present with non-empty summaries""" + errors = [] + + # Count accordions at the top level of content array + accordion_count = 0 + accordions = [] + if isinstance(content, list): + for item in content: + if isinstance(item, dict) and item.get('type') == 'accordion': + accordion_count += 1 + accordions.append(item) + + if accordion_count == 0: + errors.append("Missing accordions (required: 2)") + elif accordion_count == 1: + errors.append("Only 1 accordion found (required: 2)") + elif accordion_count > 2: + errors.append(f"Too many accordions found: {accordion_count} (required: 2)") + + # Validate accordion summaries + for idx, accordion in enumerate(accordions, 1): + summary = accordion.get('summary', '') + if not summary or summary.strip() == '': + errors.append(f"Accordion {idx}: Summary cannot be empty") + + return errors + +def validate_images(content): + """Validate that at least 1 image is present with proper alt text and non-placeholder src""" + errors = [] + + # Find all images (both standalone and within other components) + all_images = find_content_items(content, 'image') + + if not all_images: + errors.append("Missing image (at least 1 required)") + return errors + + for idx, image in enumerate(all_images, 1): + # Validate alt text + alt = image.get('alt', '') + + if not alt or alt.strip() == '': + errors.append(f"Image {idx}: Alt text cannot be empty") + elif alt == "Add your alt text here": + errors.append(f"Image {idx}: Alt text cannot be the default text 'Add your alt text here'") + + # Validate src + src = image.get('src', '') + + if src == "/placeholder_no_image.png": + errors.append(f"Image {idx}: Image source cannot be the placeholder '/placeholder_no_image.png'") + + return errors + +def validate_infocards(content): + """Validate that infocards component is present and each card has proper title, imageAlt, and imageUrl""" + errors = [] + infocards = find_content_items(content, 'infocards') + + # Check if infocards component exists + if not infocards: + errors.append("Missing infocards component") + return errors + + for infocard_idx, infocard in enumerate(infocards, 1): + cards = infocard.get('cards', []) + + if not cards: + errors.append(f"Infocards {infocard_idx}: No cards found") + continue + + for card_idx, card in enumerate(cards, 1): + # Validate title + title = card.get('title', '') + if not title or title.strip() == '': + errors.append(f"Infocards {infocard_idx}, Card {card_idx}: Title cannot be empty") + + # Validate imageAlt + image_alt = card.get('imageAlt', '') + if not image_alt or image_alt.strip() == '': + errors.append(f"Infocards {infocard_idx}, Card {card_idx}: Image alt text cannot be empty") + elif image_alt == "A placeholder image.": + errors.append(f"Infocards {infocard_idx}, Card {card_idx}: Image alt text cannot be the placeholder text 'A placeholder image.'") + + # Validate imageUrl + image_url = card.get('imageUrl', '') + if not image_url or image_url.strip() == '': + errors.append(f"Infocards {infocard_idx}, Card {card_idx}: Image URL cannot be empty") + + return errors + +def validate_content_not_empty(content): + """Validate that content array is not empty""" + errors = [] + + if not content or len(content) == 0: + errors.append("Content array cannot be empty") + + return errors + +def validate_page_json(json_data, filename): + """Validate page JSON with all requirements""" + errors = [] + page_title = "N/A" + + try: + # Extract page title + page_title = json_data['page'].get('title', 'N/A') + + # Validate summary + summary = json_data['page']['contentPageHeader']['summary'] + + if summary.strip() == '': + errors.append("Summary cannot be empty or contain only whitespace") + elif summary == "This is the page summary": + errors.append("Summary cannot be the default text 'This is the page summary'") + + # Get content for validation + content = json_data.get('content', []) + + # Validate content is not empty + content_empty_errors = validate_content_not_empty(content) + errors.extend(content_empty_errors) + + # Only continue validation if content is not empty + if not content_empty_errors: + # Validate heading hierarchy across entire document + heading_errors = validate_heading_hierarchy(content) + errors.extend(heading_errors) + + # Validate prose blocks with lists + prose_errors = validate_prose_blocks_with_lists(content) + errors.extend(prose_errors) + + # Validate nested lists + nested_list_errors = validate_nested_lists(content) + errors.extend(nested_list_errors) + + # Validate tables (at least 1, validate all if multiple) + table_errors = validate_table_structure_and_caption(content) + errors.extend(table_errors) + + # Validate accordions (exactly 2) + accordion_errors = validate_accordions(content) + errors.extend(accordion_errors) + + # Validate images (at least 1, validate all if multiple) + image_errors = validate_images(content) + errors.extend(image_errors) + + # Validate infocards + infocard_errors = validate_infocards(content) + errors.extend(infocard_errors) + + except KeyError as e: + errors.append(f"Missing required field: {str(e)}") + + return { + 'valid': len(errors) == 0, + 'errors': errors, + 'title': page_title + } + +def validate_json_file(file_path): + """Validate a single JSON file""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + json_data = json.load(f) + + filename = os.path.basename(file_path) + return validate_page_json(json_data, filename) + + except json.JSONDecodeError as e: + return { + 'valid': False, + 'errors': [f"Invalid JSON format: {str(e)}"], + 'title': 'N/A' + } + except Exception as e: + return { + 'valid': False, + 'errors': [f"Error reading file: {str(e)}"], + 'title': 'N/A' + } + +def generate_csv_report(results, directory): + """Generate CSV report inside project folder (json-assignment-validator/reports)""" + + # 🔹 Get the directory where marking.py is located + base_dir = Path(__file__).resolve().parent + + # 🔹 Create reports folder inside project directory + reports_dir = base_dir / 'reports' + reports_dir.mkdir(exist_ok=True) + + folder_name = os.path.basename(os.path.abspath(directory)) + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + report_filename = reports_dir / f"{folder_name}_report_{timestamp}.csv" + + # 🔹 Sort files: PASS first, FAIL after + sorted_files = sorted( + results['files'], + key=lambda x: (not x['valid'], x['title']) # PASS first, then alphabetical by title + ) + + with open(report_filename, 'w', newline='', encoding='utf-8') as csvfile: + fieldnames = ['Name', 'Status', 'Errors'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + writer.writeheader() + + for file_result in sorted_files: + if file_result['errors']: + writer.writerow({ + 'Name': file_result['title'], + 'Status': 'FAIL', + 'Errors': file_result['errors'][0] + }) + + for error in file_result['errors'][1:]: + writer.writerow({ + 'Name': '', + 'Status': '', + 'Errors': error + }) + else: + writer.writerow({ + 'Name': file_result['title'], + 'Status': 'PASS', + 'Errors': '' + }) + + return str(report_filename) + +def validate_multiple_files(directory): + """Validate all JSON files in a directory (summary-only output)""" + results = { + 'total': 0, + 'passed': 0, + 'failed': 0, + 'files': [] + } + + try: + directory_path = Path(directory) + json_files = list(directory_path.glob('*.json')) + + if not json_files: + print(f"No JSON files found in {directory}") + return results + + for file_path in json_files: + result = validate_json_file(file_path) + + results['total'] += 1 + + if result['valid']: + results['passed'] += 1 + else: + results['failed'] += 1 + + results['files'].append({ + 'filename': file_path.name, + 'title': result['title'], + 'path': str(file_path), + 'valid': result['valid'], + 'errors': result['errors'] + }) + + # Clean summary only + # Clean summary only with percentages + print("\nValidation Complete") + print("=" * 40) + + total = results['total'] + passed = results['passed'] + failed = results['failed'] + + pass_percentage = (passed / total * 100) if total > 0 else 0 + fail_percentage = (failed / total * 100) if total > 0 else 0 + + print(f"Total files checked: {total}") + print(f"Passed: {passed} ({pass_percentage:.1f}%)") + print(f"Failed: {failed} ({fail_percentage:.1f}%)") + + print("=" * 40) + + # Generate CSV report + if results['total'] > 0: + generate_csv_report(results, directory) + + return results + + except Exception as e: + print(f"Error reading directory: {str(e)}") + return results + +def main(): + """Main execution""" + directory = sys.argv[1] if len(sys.argv) > 1 else '.' + + print(f"Validating JSON files in: {os.path.abspath(directory)}\n") + + results = validate_multiple_files(directory) + + # Exit with error code if any files failed + sys.exit(1 if results['failed'] > 0 else 0) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/json-assignment-validator/test-examples/fail-example.json b/json-assignment-validator/test-examples/fail-example.json new file mode 100644 index 00000000..b6db9218 --- /dev/null +++ b/json-assignment-validator/test-examples/fail-example.json @@ -0,0 +1,773 @@ +{ + "comment": "Example FAIL: user attempted the task but they were missing components", + "sandboxReference": "https://sandbox-studio.isomer.gov.sg/sites/117/pages/21640", + "page": { + "title": "Fail example", + "permalink": "/moh-upec/chang-wei-si/my-page", + "lastModified": "2026-01-20T03:06:25.279Z", + "contentPageHeader": { + "summary": "This is a sample page, used for Isomer studio training", + "buttonUrl": "https://google.com", + "buttonLabel": "A CTA button can be added here", + "showThumbnail": false + } + }, + "layout": "content", + "content": [ + { + "type": "prose", + "content": [ + { + "type": "heading", + "attrs": { + "dir": "ltr", + "level": 2 + }, + "content": [ + { + "text": "Section Heading One", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Government services play a vital role in supporting citizens and enhancing community well-being. From healthcare and education to infrastructure and public safety, these services form the backbone of our society.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "The government is committed to improving the lives of citizens through various services and initiatives. Key focus areas include:", + "type": "text" + } + ] + }, + { + "type": "unorderedList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Ensuring access to quality healthcare", + "type": "text" + } + ] + }, + { + "type": "unorderedList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Implementing safety measures in public spaces", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Promoting community wellness programmes", + "type": "text" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Enhancing the education system at all levels", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Providing skills upgrading opportunities", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Supporting research and innovation", + "type": "text" + } + ] + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": null + } + }, + { + "type": "table", + "attrs": { + "caption": "Table 1: Information on Header 1, 2, and 3" + }, + "content": [ + { + "type": "tableRow", + "content": [ + { + "type": "tableHeader", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Header 1", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableHeader", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Header 2", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableHeader", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Header 3", + "type": "text" + } + ] + } + ] + } + ] + }, + { + "type": "tableRow", + "content": [ + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 1", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 2", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 3", + "type": "text" + } + ] + } + ] + } + ] + }, + { + "type": "tableRow", + "content": [ + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 4", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 5", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 6", + "type": "text" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "accordion", + "details": { + "type": "prose", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Government services play a vital role in supporting citizens and enhancing community well-being. From healthcare and education to infrastructure and public safety, these services form the backbone of our society.", + "type": "text" + } + ] + } + ] + }, + "summary": "Accordion 1" + }, + { + "type": "accordion", + "details": { + "type": "prose", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Government services play a vital role in supporting citizens and enhancing community well-being. From healthcare and education to infrastructure and public safety, these services form the backbone of our society.", + "type": "text" + } + ] + } + ] + }, + "summary": "Accordion 2" + }, + { + "alt": "A sun image", + "src": "/117/d49560da-5430-4f2f-acb6-144c43125884/download.jpeg", + "size": "default", + "type": "image", + "caption": "A sun" + }, + { + "type": "prose", + "content": [ + { + "type": "heading", + "attrs": { + "dir": "ltr", + "level": 2 + }, + "content": [ + { + "text": "Section heading two", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "The government is committed to improving the lives of citizens through various services and initiatives. Key focus areas include:", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Public Health and Safety", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Ensuring access to quality healthcare", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Implementing safety measures in public spaces", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Promoting community wellness programmes", + "type": "text" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Education and Lifelong Learning", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Enhancing the education system at all levels", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Providing skills upgrading opportunities", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Supporting research and innovation", + "type": "text" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Infrastructure Development", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Improving public transportation networks", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Upgrading community facilities", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Enhancing digital infrastructure for better connectivity", + "type": "text" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "prose", + "content": [ + { + "type": "heading", + "attrs": { + "dir": "ltr", + "level": 2 + }, + "content": [ + { + "text": "This is a title of the Infocards component", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "This is an optional subtitle for the Infocards component", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": null + } + } + ] + } + ], + "version": "0.1.0" +} \ No newline at end of file diff --git a/json-assignment-validator/test-examples/half-complete-example.json b/json-assignment-validator/test-examples/half-complete-example.json new file mode 100644 index 00000000..761ef72d --- /dev/null +++ b/json-assignment-validator/test-examples/half-complete-example.json @@ -0,0 +1,514 @@ +{ + "comment": "Example FAIL: user attempted the task but did not complete all required steps", + "sandboxReference": "https://sandbox-studio.isomer.gov.sg/sites/118/pages/21866", + "page": { + "title": "Half complete example", + "permalink": "/my-ostin/my-page", + "lastModified": "2026-02-10T03:00:45.955Z", + "contentPageHeader": { + "summary": "This is a sample page, used for Isomer studio training", + "buttonUrl": "mailto:Blah", + "buttonLabel": "A CTA button can be added here ", + "showThumbnail": false + } + }, + "layout": "content", + "content": [ + { + "type": "prose", + "content": [ + { + "type": "heading", + "attrs": { + "dir": "ltr", + "level": 2 + }, + "content": [ + { + "text": "Section heading one", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "This is the content under this section heading one ", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Next paragraph under this section with", + "type": "text" + } + ] + }, + { + "type": "unorderedList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Bullet 1 to elaborate ", + "type": "text" + } + ] + }, + { + "type": "unorderedList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "And sub bullets to expand ", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "And another sub bullet", + "type": "text" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "bullet 2 ", + "type": "text" + } + ] + }, + { + "type": "unorderedList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "And so on", + "type": "text" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": null + } + }, + { + "type": "paragraph", + "attrs": { + "dir": null + } + }, + { + "type": "table", + "attrs": { + "caption": "Table 1: Information on Header 1, 2 and 3" + }, + "content": [ + { + "type": "tableRow", + "content": [ + { + "type": "tableHeader", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Header 1", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableHeader", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Header 2", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableHeader", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Header 3", + "type": "text" + } + ] + } + ] + } + ] + }, + { + "type": "tableRow", + "content": [ + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 1", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": null + } + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": null + } + } + ] + } + ] + }, + { + "type": "tableRow", + "content": [ + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": null + } + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": null + } + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": null + } + } + ] + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": null + } + }, + { + "type": "paragraph", + "attrs": { + "dir": null + } + } + ] + }, + { + "type": "accordion", + "details": { + "type": "prose", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "This is the content under Accordion 1", + "type": "text" + } + ] + } + ] + }, + "summary": "Accordion 1" + }, + { + "type": "accordion", + "details": { + "type": "prose", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "This appears under Accordion 2", + "type": "text" + } + ] + } + ] + }, + "summary": "Accordion 2" + }, + { + "alt": "This is a placeholder with no image ", + "src": "/placeholder_no_image.png", + "size": "default", + "type": "image", + "caption": "A brief description of this non-existent image for us to practice on" + }, + { + "type": "prose", + "content": [ + { + "type": "heading", + "attrs": { + "dir": "ltr", + "level": 2 + }, + "content": [ + { + "text": "Section heading two", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "More text for this section", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Blah blah ", + "type": "text" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "infocards", + "cards": [ + { + "url": "https://www.google.com", + "title": "This is the first card", + "imageAlt": "This is the alt text", + "imageFit": "cover", + "imageUrl": "/placeholder_no_image.png", + "description": "1st pic " + }, + { + "url": "https://www.google.com", + "title": "This is the second card", + "imageAlt": "This is the alt text", + "imageFit": "cover", + "imageUrl": "/placeholder_no_image.png", + "description": "Pic 2" + }, + { + "url": "https://www.google.com", + "title": "This is the third card", + "imageAlt": "This is the alt text", + "imageFit": "cover", + "imageUrl": "/placeholder_no_image.png", + "description": "Pic 3" + } + ], + "title": "Infocards for 3 key events/CTA", + "variant": "cardsWithImages", + "subtitle": "These are our latest news", + "maxColumns": "3" + } + ], + "version": "0.1.0" +} \ No newline at end of file diff --git a/json-assignment-validator/test-examples/perfect-example.json b/json-assignment-validator/test-examples/perfect-example.json new file mode 100644 index 00000000..14b282e6 --- /dev/null +++ b/json-assignment-validator/test-examples/perfect-example.json @@ -0,0 +1,786 @@ +{ + "comment": "Example PASS: user attempted the task and completed all required steps", + "sandboxReference": "https://sandbox-studio.isomer.gov.sg/sites/118/pages/21857", + "page": { + "title": "Perfect example", + "permalink": "/jun-kun/my-page", + "lastModified": "2026-01-21T02:37:41.307Z", + "contentPageHeader": { + "summary": "This is a sample page, used for Isomer studio training.", + "buttonUrl": "https://isomer.gov.sg/", + "buttonLabel": "A CTA button can be added here", + "showThumbnail": false + } + }, + "layout": "content", + "content": [ + { + "type": "prose", + "content": [ + { + "type": "heading", + "attrs": { + "dir": "ltr", + "level": 2 + }, + "content": [ + { + "text": "Section heading one", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Government services play a vital role in supporting citizens and enhancing community well-being. From healthcare and education to infrastructure and public safety, these services form the backbone of our society.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "The government is committed to improving the lives of citizens through various services and initiatives. Key focus areas include:", + "type": "text" + } + ] + }, + { + "type": "unorderedList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Ensuring access to quality healthcare", + "type": "text" + } + ] + }, + { + "type": "unorderedList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Implementing safety measures in public spaces", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Promoting community wellness programmes", + "type": "text" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Enhancing the education system at all levels", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Providing skills upgrading opportunities", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Supporting research and innovation", + "type": "text" + } + ] + } + ] + } + ] + }, + { + "type": "table", + "attrs": { + "caption": "Table 1: Information on Header 1, 2, and 3" + }, + "content": [ + { + "type": "tableRow", + "content": [ + { + "type": "tableHeader", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Header 1", + "type": "text", + "marks": [ + { + "type": "bold" + } + ] + } + ] + } + ] + }, + { + "type": "tableHeader", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Header 2", + "type": "text", + "marks": [ + { + "type": "bold" + } + ] + } + ] + } + ] + }, + { + "type": "tableHeader", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Header 3", + "type": "text", + "marks": [ + { + "type": "bold" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "tableRow", + "content": [ + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 1", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 2", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 3", + "type": "text" + } + ] + } + ] + } + ] + }, + { + "type": "tableRow", + "content": [ + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 4", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 5", + "type": "text" + } + ] + } + ] + }, + { + "type": "tableCell", + "attrs": { + "colspan": 1, + "rowspan": 1, + "colwidth": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Item 6", + "type": "text" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "accordion", + "details": { + "type": "prose", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Government services play a vital role in supporting citizens and enhancing community well-being. From healthcare and education to infrastructure and public safety, these services form the backbone of our society.", + "type": "text" + } + ] + } + ] + }, + "summary": "Accordion 1" + }, + { + "type": "accordion", + "details": { + "type": "prose", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Government services play a vital role in supporting citizens and enhancing community well-being. From healthcare and education to infrastructure and public safety, these services form the backbone of our society.", + "type": "text" + } + ] + } + ] + }, + "summary": "Accordion" + }, + { + "alt": "Please upload an image of your choice here and write an alt text for the image", + "src": "/118/39a1dca3-8d3d-40ab-870a-6e1601367b95/image-27cd6af3.webp", + "size": "default", + "type": "image" + }, + { + "type": "prose", + "content": [ + { + "type": "heading", + "attrs": { + "dir": "ltr", + "level": 2 + }, + "content": [ + { + "text": "Section heading two", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "The government is committed to improving the lives of citizens through various services and initiatives. Key focus areas include:", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Public Health and Safety", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Ensuring access to quality healthcare", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Implementing safety measures in public spaces", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Promoting community wellness programmes", + "type": "text" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Education and Lifelong Learning", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Enhancing the education system at all levels", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Providing skills upgrading opportunities", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Supporting research and innovation", + "type": "text" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Infrastructure Development", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Improving public transportation networks", + "type": "text" + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Upgrading community facilities", + "type": "text" + } + ] + }, + { + "type": "orderedList", + "attrs": { + "type": null, + "start": 1 + }, + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "dir": "ltr" + }, + "content": [ + { + "text": "Enhancing digital infrastructure for better connectivity", + "type": "text" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "infocards", + "cards": [ + { + "url": "https://isomer.gov.sg/", + "title": "This is the first card", + "imageAlt": "This is the alt text", + "imageFit": "cover", + "imageUrl": "/118/c161fbee-b755-43e7-8393-a92b8d002ccc/image-27cd6af3.webp" + }, + { + "url": "https://isomer.gov.sg/", + "title": "This is the second card", + "imageAlt": "This is the alt text", + "imageFit": "cover", + "imageUrl": "/118/fb741c5a-8d3a-4e45-82b6-dbe873b7c9fd/image-27cd6af3.webp" + }, + { + "url": "https://isomer.gov.sg/", + "title": "This is the third card", + "imageAlt": "This is the alt text", + "imageFit": "cover", + "imageUrl": "/118/4502e9a0-b320-46df-83b3-b69ab13a6073/image-27cd6af3.webp" + } + ], + "title": "This is a title of the Infocards component", + "variant": "cardsWithImages", + "subtitle": "This is an optional subtitle for the Infocards component", + "maxColumns": "3" + }, + { + "url": "https://www.youtube.com/embed/dQw4w9WgXcQ?si=ggGGn4uvFWAIelWD", + "type": "video", + "title": "Rick Astley - Never Gonna Give You Up" + }, + { + "type": "blockquote", + "quote": "Let me gooooo", + "source": "Elsa", + "imageAlt": "Portrait of Huaying Zhu" + } + ], + "version": "0.1.0" +} \ No newline at end of file