From 2dfb268d4153ae5a92360d251c3b3b3953b6073c Mon Sep 17 00:00:00 2001 From: tingshian9 <105689900+tingshian9@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:54:00 +0800 Subject: [PATCH 01/10] Create ReadME.md --- json-assignment-validator/ReadME.md | 229 ++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 json-assignment-validator/ReadME.md diff --git a/json-assignment-validator/ReadME.md b/json-assignment-validator/ReadME.md new file mode 100644 index 00000000..a005f470 --- /dev/null +++ b/json-assignment-validator/ReadME.md @@ -0,0 +1,229 @@ +# R-U-Certified JSON Validator + +A validation tool developed by Ting Shian to assess whether participants who attended Isomer training have successfully met the assignment requirements. + +The tool evaluates submitted JSON files against predefined criteria and automatically generates a CSV report summarising the validation outcomes. + +--- + +## ๐Ÿ“ Project Structure + +```bash +r-u-certified/ +โ”‚ +โ”œโ”€โ”€ marking.py # Main validation script +โ”œโ”€โ”€ assignment-submission/ # Folder for JSON submissions +โ”œโ”€โ”€ test-examples/ # Example JSON files (pass / fail samples) +โ”œโ”€โ”€ reports/ # Auto-generated CSV reports (created at runtime) +โ””โ”€โ”€ .git/ # Git metadata +``` + +--- + +## ๐Ÿš€ Features + +The validator checks the following requirements: + +### 1๏ธโƒฃ Page-Level Validation + +* Page title extraction +* Summary must not be empty +* Summary must not use default placeholder text + +### 2๏ธโƒฃ Content-Level Validation + +* Content array must not be empty + +### 3๏ธโƒฃ Headings + +* First heading must be **Level 2** +* Heading hierarchy cannot skip levels + +### 4๏ธโƒฃ Prose Blocks + +* Minimum **2 prose blocks** required +* At least one prose block must contain: + + * An unordered list + * An ordered list + +### 5๏ธโƒฃ Nested Lists + +* Unordered lists must contain nested sub-lists +* Ordered lists must contain nested sub-lists + +### 6๏ธโƒฃ Tables + +* At least one table required +* Each table must: + + * Have a non-empty caption + * Not use default caption text + * Include table headers (`tableHeader`) + * Include table cells (`tableCell`) + +### 7๏ธโƒฃ Accordions + +* Exactly **2 accordions** required +* Each accordion must have a non-empty summary + +### 8๏ธโƒฃ Images + +* At least one image required +* Image must: + + * Have non-empty alt text + * Not use placeholder alt text + * Not use placeholder image source + +### 9๏ธโƒฃ Infocards Component + +* Infocards component must exist +* Each card must contain: + + * Title + * Image alt text + * Image URL + +--- + +## ๐Ÿ›  Requirements + +* Python 3.8+ +* No external dependencies (standard library only) + +Modules used: + +* `json` +* `os` +* `sys` +* `csv` +* `pathlib` +* `datetime` + +--- + +## โ–ถ๏ธ How to Use + +### Exporting Files from Isomer Studio (Sandbox) + +To validate assignment submissions: +
    +
  1. An engineer must export the page JSON files from the Isomer Studio Sandbox environment.
  2. +
  3. Create an empty assignment-submission folder +
  4. Place the exported JSON files into the assignment-submission/ folder.
  5. +
  6. Run the validation script against that folder.
  7. +
+ +This ensures that the submitted files are evaluated using the same structured validation criteria. + +### Validate JSON files in a folder + +```bash +python marking.py assignment-submission +``` + +--- + +## ๐Ÿ“Š Output + +### Console Output + +The script prints: + +* PASS / FAIL per file +* Detailed validation errors +* Summary statistics + +Example: + +```bash +Found 3 JSON file(s) to validate + +โŒ FAIL: example.json + - First heading must be level 2 + - Missing infocards component + +SUMMARY: + Total files: 3 + โœ… Passed: 1 + โŒ Failed: 2 +``` + +--- + +### CSV Report + +After validation, a CSV report is automatically generated in the `reports/` folder. + +Filename format: + +```bash +_report_YYYYMMDD_HHMMSS.csv +``` + +CSV Structure: + +| Name | Status | Errors | +| ---------- | ----------- | ----------------- | +| Page Title | PASS / FAIL | First error | +| | | Additional errors | + +--- + +## ๐Ÿงช Test Examples + +Located in: + +```bash +test-examples/ +``` + +Includes: + +* `perfect-example.json` +* `half-complete-example.json` +* `fail-example.json` + +You can use these files to test how the validator behaves under different scenarios. + +--- + +## ๐Ÿ— Architecture Overview + +### Core Validation Flow + +1. Load JSON file +2. Validate page-level fields +3. Validate content structure +4. Run component-specific validators +5. Collect errors +6. Generate summary + CSV report + +### Key Functions + +* `validate_page_json()` โ€” Runs full validation pipeline +* `validate_multiple_files()` โ€” Handles directory validation +* `generate_csv_report()` โ€” Creates structured CSV output +* `find_content_items()` โ€” Recursive utility for locating content types + +--- + +## ๐ŸŽฏ Intended Use Cases + +* Assignment marking automation +* Structured content validation +* JSON schema compliance checking +* Batch validation for content migration workflows + +--- + +## ๐Ÿ“ฌ Extending the Validator + +To add new validation rules: + +1. Create a new validation function +2. Append its errors inside `validate_page_json()` +3. Ensure error messages are clear and actionable + +--- From 0afc551daf64317ffa7d0ae5bb86e82340aaf7b9 Mon Sep 17 00:00:00 2001 From: tingshian9 <105689900+tingshian9@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:54:31 +0800 Subject: [PATCH 02/10] Add files via upload --- json-assignment-validator/marking.py | 468 +++++++++++++++++++++++++++ 1 file changed, 468 insertions(+) create mode 100644 json-assignment-validator/marking.py diff --git a/json-assignment-validator/marking.py b/json-assignment-validator/marking.py new file mode 100644 index 00000000..401c36a7 --- /dev/null +++ b/json-assignment-validator/marking.py @@ -0,0 +1,468 @@ +# 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 in reports folder with errors on separate rows""" + # Create reports folder if it doesn't exist + reports_dir = Path('reports') + reports_dir.mkdir(exist_ok=True) + + # Create report filename based on folder name + 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" + + # Write CSV report + 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 results['files']: + if file_result['errors']: + # Write first error with name and status + writer.writerow({ + 'Name': file_result['title'], + 'Status': 'PASS' if file_result['valid'] else 'FAIL', + 'Errors': file_result['errors'][0] + }) + # Write remaining errors with empty name and status + for error in file_result['errors'][1:]: + writer.writerow({ + 'Name': '', + 'Status': '', + 'Errors': error + }) + else: + # No errors, just write the single row + writer.writerow({ + 'Name': file_result['title'], + 'Status': 'PASS', + 'Errors': '' + }) + + print(f"\n๐Ÿ“„ CSV report generated: {report_filename}") + return str(report_filename) + +def validate_multiple_files(directory): + """Validate all JSON files in a directory""" + 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 + + print(f"Found {len(json_files)} JSON file(s) to validate\n") + print("=" * 60) + + for file_path in json_files: + filename = file_path.name + result = validate_json_file(file_path) + + results['total'] += 1 + + if result['valid']: + results['passed'] += 1 + print(f"โœ… PASS: {filename} (Title: {result['title']})") + else: + results['failed'] += 1 + print(f"โŒ FAIL: {filename} (Title: {result['title']})") + for error in result['errors']: + print(f" - {error}") + + results['files'].append({ + 'filename': filename, + 'title': result['title'], + 'path': str(file_path), + 'valid': result['valid'], + 'errors': result['errors'] + }) + + # Summary + print("\n" + "=" * 60) + print("SUMMARY:") + print(f" Total files: {results['total']}") + print(f" โœ… Passed: {results['passed']}") + print(f" โŒ Failed: {results['failed']}") + print("=" * 60) + + # 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 From 9f3f61116537d2095bdaa202b069e4e53de777f6 Mon Sep 17 00:00:00 2001 From: tingshian9 <105689900+tingshian9@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:55:21 +0800 Subject: [PATCH 03/10] Create test-examples.md --- json-assignment-validator/test-examples.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 json-assignment-validator/test-examples.md diff --git a/json-assignment-validator/test-examples.md b/json-assignment-validator/test-examples.md new file mode 100644 index 00000000..038d718d --- /dev/null +++ b/json-assignment-validator/test-examples.md @@ -0,0 +1 @@ +testing From 5790e10e83fd02be3bf3acbc0f25a950cd1eb3dd Mon Sep 17 00:00:00 2001 From: tingshian9 <105689900+tingshian9@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:55:51 +0800 Subject: [PATCH 04/10] Rename json-assignment-validator/test-examples.md to json-assignment-validator/test-examples/test-examples.md --- json-assignment-validator/{ => test-examples}/test-examples.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename json-assignment-validator/{ => test-examples}/test-examples.md (100%) diff --git a/json-assignment-validator/test-examples.md b/json-assignment-validator/test-examples/test-examples.md similarity index 100% rename from json-assignment-validator/test-examples.md rename to json-assignment-validator/test-examples/test-examples.md From 88bbf28f0fb0f2299531a9141a7670d0c7c98dc7 Mon Sep 17 00:00:00 2001 From: tingshian9 <105689900+tingshian9@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:56:19 +0800 Subject: [PATCH 05/10] Add files via upload --- .../test-examples/fail-example.json | 773 +++++++++++++++++ .../test-examples/half-complete-example.json | 514 ++++++++++++ .../test-examples/perfect-example.json | 786 ++++++++++++++++++ 3 files changed, 2073 insertions(+) create mode 100644 json-assignment-validator/test-examples/fail-example.json create mode 100644 json-assignment-validator/test-examples/half-complete-example.json create mode 100644 json-assignment-validator/test-examples/perfect-example.json 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 From 036e8e2ed72f3bd96514cd3b1fe302c826ba7716 Mon Sep 17 00:00:00 2001 From: tingshian9 <105689900+tingshian9@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:56:48 +0800 Subject: [PATCH 06/10] Delete json-assignment-validator/test-examples/test-examples.md --- json-assignment-validator/test-examples/test-examples.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 json-assignment-validator/test-examples/test-examples.md diff --git a/json-assignment-validator/test-examples/test-examples.md b/json-assignment-validator/test-examples/test-examples.md deleted file mode 100644 index 038d718d..00000000 --- a/json-assignment-validator/test-examples/test-examples.md +++ /dev/null @@ -1 +0,0 @@ -testing From f6b41057dddb57757bd9b5e973091a3cc0757ca2 Mon Sep 17 00:00:00 2001 From: tingshian9 <105689900+tingshian9@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:48:19 +0800 Subject: [PATCH 07/10] Delete json-assignment-validator/ReadME.md --- json-assignment-validator/ReadME.md | 229 ---------------------------- 1 file changed, 229 deletions(-) delete mode 100644 json-assignment-validator/ReadME.md diff --git a/json-assignment-validator/ReadME.md b/json-assignment-validator/ReadME.md deleted file mode 100644 index a005f470..00000000 --- a/json-assignment-validator/ReadME.md +++ /dev/null @@ -1,229 +0,0 @@ -# R-U-Certified JSON Validator - -A validation tool developed by Ting Shian to assess whether participants who attended Isomer training have successfully met the assignment requirements. - -The tool evaluates submitted JSON files against predefined criteria and automatically generates a CSV report summarising the validation outcomes. - ---- - -## ๐Ÿ“ Project Structure - -```bash -r-u-certified/ -โ”‚ -โ”œโ”€โ”€ marking.py # Main validation script -โ”œโ”€โ”€ assignment-submission/ # Folder for JSON submissions -โ”œโ”€โ”€ test-examples/ # Example JSON files (pass / fail samples) -โ”œโ”€โ”€ reports/ # Auto-generated CSV reports (created at runtime) -โ””โ”€โ”€ .git/ # Git metadata -``` - ---- - -## ๐Ÿš€ Features - -The validator checks the following requirements: - -### 1๏ธโƒฃ Page-Level Validation - -* Page title extraction -* Summary must not be empty -* Summary must not use default placeholder text - -### 2๏ธโƒฃ Content-Level Validation - -* Content array must not be empty - -### 3๏ธโƒฃ Headings - -* First heading must be **Level 2** -* Heading hierarchy cannot skip levels - -### 4๏ธโƒฃ Prose Blocks - -* Minimum **2 prose blocks** required -* At least one prose block must contain: - - * An unordered list - * An ordered list - -### 5๏ธโƒฃ Nested Lists - -* Unordered lists must contain nested sub-lists -* Ordered lists must contain nested sub-lists - -### 6๏ธโƒฃ Tables - -* At least one table required -* Each table must: - - * Have a non-empty caption - * Not use default caption text - * Include table headers (`tableHeader`) - * Include table cells (`tableCell`) - -### 7๏ธโƒฃ Accordions - -* Exactly **2 accordions** required -* Each accordion must have a non-empty summary - -### 8๏ธโƒฃ Images - -* At least one image required -* Image must: - - * Have non-empty alt text - * Not use placeholder alt text - * Not use placeholder image source - -### 9๏ธโƒฃ Infocards Component - -* Infocards component must exist -* Each card must contain: - - * Title - * Image alt text - * Image URL - ---- - -## ๐Ÿ›  Requirements - -* Python 3.8+ -* No external dependencies (standard library only) - -Modules used: - -* `json` -* `os` -* `sys` -* `csv` -* `pathlib` -* `datetime` - ---- - -## โ–ถ๏ธ How to Use - -### Exporting Files from Isomer Studio (Sandbox) - -To validate assignment submissions: -
    -
  1. An engineer must export the page JSON files from the Isomer Studio Sandbox environment.
  2. -
  3. Create an empty assignment-submission folder -
  4. Place the exported JSON files into the assignment-submission/ folder.
  5. -
  6. Run the validation script against that folder.
  7. -
- -This ensures that the submitted files are evaluated using the same structured validation criteria. - -### Validate JSON files in a folder - -```bash -python marking.py assignment-submission -``` - ---- - -## ๐Ÿ“Š Output - -### Console Output - -The script prints: - -* PASS / FAIL per file -* Detailed validation errors -* Summary statistics - -Example: - -```bash -Found 3 JSON file(s) to validate - -โŒ FAIL: example.json - - First heading must be level 2 - - Missing infocards component - -SUMMARY: - Total files: 3 - โœ… Passed: 1 - โŒ Failed: 2 -``` - ---- - -### CSV Report - -After validation, a CSV report is automatically generated in the `reports/` folder. - -Filename format: - -```bash -_report_YYYYMMDD_HHMMSS.csv -``` - -CSV Structure: - -| Name | Status | Errors | -| ---------- | ----------- | ----------------- | -| Page Title | PASS / FAIL | First error | -| | | Additional errors | - ---- - -## ๐Ÿงช Test Examples - -Located in: - -```bash -test-examples/ -``` - -Includes: - -* `perfect-example.json` -* `half-complete-example.json` -* `fail-example.json` - -You can use these files to test how the validator behaves under different scenarios. - ---- - -## ๐Ÿ— Architecture Overview - -### Core Validation Flow - -1. Load JSON file -2. Validate page-level fields -3. Validate content structure -4. Run component-specific validators -5. Collect errors -6. Generate summary + CSV report - -### Key Functions - -* `validate_page_json()` โ€” Runs full validation pipeline -* `validate_multiple_files()` โ€” Handles directory validation -* `generate_csv_report()` โ€” Creates structured CSV output -* `find_content_items()` โ€” Recursive utility for locating content types - ---- - -## ๐ŸŽฏ Intended Use Cases - -* Assignment marking automation -* Structured content validation -* JSON schema compliance checking -* Batch validation for content migration workflows - ---- - -## ๐Ÿ“ฌ Extending the Validator - -To add new validation rules: - -1. Create a new validation function -2. Append its errors inside `validate_page_json()` -3. Ensure error messages are clear and actionable - ---- From 727b31ef80d5fa6aeab9faeaf1556f985cc1148c Mon Sep 17 00:00:00 2001 From: tingshian9 <105689900+tingshian9@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:48:48 +0800 Subject: [PATCH 08/10] Add files via upload --- json-assignment-validator/ReadME.md | 190 ++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 json-assignment-validator/ReadME.md 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 + +--- + From 564af726ca9cf4b816fe9ddd7e88e5b930f31804 Mon Sep 17 00:00:00 2001 From: tingshian9 <105689900+tingshian9@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:14:30 +0800 Subject: [PATCH 09/10] Delete json-assignment-validator/marking.py --- json-assignment-validator/marking.py | 468 --------------------------- 1 file changed, 468 deletions(-) delete mode 100644 json-assignment-validator/marking.py diff --git a/json-assignment-validator/marking.py b/json-assignment-validator/marking.py deleted file mode 100644 index 401c36a7..00000000 --- a/json-assignment-validator/marking.py +++ /dev/null @@ -1,468 +0,0 @@ -# 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 in reports folder with errors on separate rows""" - # Create reports folder if it doesn't exist - reports_dir = Path('reports') - reports_dir.mkdir(exist_ok=True) - - # Create report filename based on folder name - 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" - - # Write CSV report - 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 results['files']: - if file_result['errors']: - # Write first error with name and status - writer.writerow({ - 'Name': file_result['title'], - 'Status': 'PASS' if file_result['valid'] else 'FAIL', - 'Errors': file_result['errors'][0] - }) - # Write remaining errors with empty name and status - for error in file_result['errors'][1:]: - writer.writerow({ - 'Name': '', - 'Status': '', - 'Errors': error - }) - else: - # No errors, just write the single row - writer.writerow({ - 'Name': file_result['title'], - 'Status': 'PASS', - 'Errors': '' - }) - - print(f"\n๐Ÿ“„ CSV report generated: {report_filename}") - return str(report_filename) - -def validate_multiple_files(directory): - """Validate all JSON files in a directory""" - 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 - - print(f"Found {len(json_files)} JSON file(s) to validate\n") - print("=" * 60) - - for file_path in json_files: - filename = file_path.name - result = validate_json_file(file_path) - - results['total'] += 1 - - if result['valid']: - results['passed'] += 1 - print(f"โœ… PASS: {filename} (Title: {result['title']})") - else: - results['failed'] += 1 - print(f"โŒ FAIL: {filename} (Title: {result['title']})") - for error in result['errors']: - print(f" - {error}") - - results['files'].append({ - 'filename': filename, - 'title': result['title'], - 'path': str(file_path), - 'valid': result['valid'], - 'errors': result['errors'] - }) - - # Summary - print("\n" + "=" * 60) - print("SUMMARY:") - print(f" Total files: {results['total']}") - print(f" โœ… Passed: {results['passed']}") - print(f" โŒ Failed: {results['failed']}") - print("=" * 60) - - # 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 From db95594b9d4e731ecf6d8ed592441dd7e287f79a Mon Sep 17 00:00:00 2001 From: tingshian9 <105689900+tingshian9@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:16:32 +0800 Subject: [PATCH 10/10] Add files via upload --- json-assignment-validator/marking.py | 476 +++++++++++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 json-assignment-validator/marking.py 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