From 7f0f4fb9eaafe4d2bacf0d29497cf867036b0492 Mon Sep 17 00:00:00 2001 From: Raphael Odini Date: Tue, 30 Sep 2025 15:56:36 +0200 Subject: [PATCH] Basic script to generate tableschema from model --- core/utils/tableschema.py | 76 +++++++++++++++ data/schema-questions.json | 185 +++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 core/utils/tableschema.py create mode 100644 data/schema-questions.json diff --git a/core/utils/tableschema.py b/core/utils/tableschema.py new file mode 100644 index 00000000..bd1e3dda --- /dev/null +++ b/core/utils/tableschema.py @@ -0,0 +1,76 @@ +import json + +from django.db import models + + +TYPE_MAPPING = { + "AutoField": "integer", + "BigAutoField": "integer", + "BigIntegerField": "integer", + "BinaryField": "string", + "BooleanField": "boolean", + "CharField": "string", + "DateField": "date", + "DateTimeField": "datetime", + "DecimalField": "number", + "DurationField": "string", + "EmailField": "string", + "FileField": "string", + "FilePathField": "string", + "FloatField": "number", + "IntegerField": "integer", + "GenericIPAddressField": "string", + "NullBooleanField": "boolean", + "PositiveIntegerField": "integer", + "PositiveSmallIntegerField": "integer", + "SlugField": "string", + "SmallIntegerField": "integer", + "TextField": "string", + "TimeField": "time", + "URLField": "string", + "UUIDField": "string", +} + + +def generate_schema_from_model(model, exclude_fields=[]): + """ + Generate a table schema from a Django model. + The schema is a list of dictionaries, each representing a field in the model. + Each dictionary contains: + - name: the name of the field + - type: the type of the field (CharField, IntegerField, etc.) + - required: whether the field is required (not null and not blank) + - choices: if the field has choices, a list of possible values + """ + schema_name = f"schema-{model._meta.verbose_name_plural.lower()}" + schema_filename = f"{schema_name}.json" + schema = { + "$schema": "https://frictionlessdata.io/schemas/table-schema.json", + "encoding": "utf-8", + "fields": [], + "homepage": "https://github.com/quizanthropocene", + "name": schema_name, + "path": f"https://raw.githubusercontent.com/quizanthropocene/admin-backend/refs/heads/master/data/{schema_filename}", # noqa + } + + for field in model._meta.get_fields(): + # skip excluded fields + if field.name in exclude_fields: + continue + # skip auto fields + if isinstance(field, models.AutoField): + continue + # skip FK & M2M fields + if field.is_relation: + continue + field_info = { + "name": field.name, + "type": TYPE_MAPPING.get(field.get_internal_type(), "string"), + "required": not (field.null or field.blank), + } + if hasattr(field, "choices") and field.choices: + field_info["choices"] = [choice[0] for choice in field.choices] + schema["fields"].append(field_info) + + with open(f"data/{schema_filename}", "w", encoding="utf-8") as f: + json.dump(schema, f, ensure_ascii=False, indent=4) diff --git a/data/schema-questions.json b/data/schema-questions.json new file mode 100644 index 00000000..7d37e94d --- /dev/null +++ b/data/schema-questions.json @@ -0,0 +1,185 @@ +{ + "$schema": "https://frictionlessdata.io/schemas/table-schema.json", + "encoding": "utf-8", + "fields": [ + { + "name": "text", + "type": "string", + "required": true + }, + { + "name": "hint", + "type": "string", + "required": false + }, + { + "name": "type", + "type": "string", + "required": true, + "choices": [ + "QCM", + "QCM-RM", + "VF" + ] + }, + { + "name": "difficulty", + "type": "integer", + "required": true, + "choices": [ + 0, + 1, + 2, + 3, + 4 + ] + }, + { + "name": "language", + "type": "string", + "required": true, + "choices": [ + "FRENCH", + "ENGLISH", + "SPANISH", + "ITALIAN", + "GERMAN" + ] + }, + { + "name": "answer_choice_a", + "type": "string", + "required": false + }, + { + "name": "answer_choice_b", + "type": "string", + "required": false + }, + { + "name": "answer_choice_c", + "type": "string", + "required": false + }, + { + "name": "answer_choice_d", + "type": "string", + "required": false + }, + { + "name": "answer_correct", + "type": "string", + "required": false, + "choices": [ + "a", + "b", + "c", + "d", + "ab", + "ac", + "ad", + "bc", + "bd", + "cd", + "abc", + "abd", + "acd", + "bcd", + "abcd" + ] + }, + { + "name": "has_ordered_answers", + "type": "boolean", + "required": true + }, + { + "name": "answer_explanation", + "type": "string", + "required": false + }, + { + "name": "answer_audio_url", + "type": "string", + "required": false + }, + { + "name": "answer_audio_url_text", + "type": "string", + "required": false + }, + { + "name": "answer_video_url", + "type": "string", + "required": false + }, + { + "name": "answer_video_url_text", + "type": "string", + "required": false + }, + { + "name": "answer_source_accessible_url", + "type": "string", + "required": false + }, + { + "name": "answer_source_accessible_url_text", + "type": "string", + "required": false + }, + { + "name": "answer_source_scientific_url", + "type": "string", + "required": false + }, + { + "name": "answer_source_scientific_url_text", + "type": "string", + "required": false + }, + { + "name": "answer_book_recommendation", + "type": "string", + "required": false + }, + { + "name": "answer_image_url", + "type": "string", + "required": false + }, + { + "name": "answer_image_url_text", + "type": "string", + "required": false + }, + { + "name": "answer_extra_info", + "type": "string", + "required": false + }, + { + "name": "visibility", + "type": "string", + "required": true, + "choices": [ + "PUBLIC", + "HIDDEN", + "PRIVATE" + ] + }, + { + "name": "author_certify_necessary_rights", + "type": "boolean", + "required": true + }, + { + "name": "author_agree_commercial_use", + "type": "boolean", + "required": true + } + ], + "homepage": "https://github.com/quizanthropocene", + "name": "schema-questions", + "path": "https://raw.githubusercontent.com/quizanthropocene/admin-backend/refs/heads/master/data/schema-questions.json" +}