Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
duncanjbrown committed Mar 28, 2024
1 parent 93bd14e commit a6fe88b
Show file tree
Hide file tree
Showing 22 changed files with 5,932 additions and 180 deletions.
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@ CURRENT_GIT_SHA := $(shell git rev-parse HEAD | cut -c 1-8)
help: ## Show this help
@grep -E '^[a-zA-Z\.\-\_]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

## Schema documentation
consultation_analyser/consultations/public_schema.py: consultation_analyser/public_schema.yaml
poetry run datamodel-codegen --input $< --output $@
poetry run datamodel-codegen --input $< --output $@ --use-schema-description

consultation_analyser/consultations/public_schema/%_schema.json: consultation_analyser/consultations/public_schema.py
poetry run python manage.py generate_json_schemas

consultation_analyser/consultations/public_schema/%_example.json: consultation_analyser/consultations/public_schema/%_schema.json
npx generate-json $^ $@ none $(PWD)/json-schema-faker-options.js

.PHONY: update_schema_docs
update_schema_docs: consultation_analyser/consultations/public_schema/consultation_example.json consultation_analyser/consultations/public_schema/consultation_response_list_example.json

.PHONY: setup_dev_db
setup_dev_db: ## Set up the development db on a local postgres
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
from pydantic import BaseModel


Expand All @@ -12,15 +11,6 @@ def name(self):
def description(self):
return self.schema.__doc__

def example(self):
result = {}

for name, field in self.schema.model_fields.items():
result[name] = field.json_schema_extra.get("example")

if output := dict(result):
return json.dumps(output, indent=4)

def rows(self):
output = []
for field_name in self.schema.model_fields.keys():
Expand All @@ -31,10 +21,6 @@ def rows(self):
"description": field.description,
}

if field.json_schema_extra:
example = field.json_schema_extra.get("example")
field_details["example"] = example

output.append(field_details)

return output
78 changes: 58 additions & 20 deletions consultation_analyser/consultations/jinja2/schema.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,62 @@
{% block content %}
<h1 class="govuk-heading-xl">Consultation data schema v0.1</h1>

<p class="govuk-body">This page lists the data components required for a consultation to be suitable for processing by the
analyser.
</p>
<p class="govuk-body">This page lists the data components required for a consultation to be suitable for processing by the analyser. </p>

<h2 class="govuk-heading-l">Changelog</h2>
<h3 class="govuk-heading-s">28 March 2024</h3>
<p class="govuk-body">Initial release</p>
<p class="govuk-body">Data should be provided in two JSON files: one for the consultation and one for its answers. Those two data files are described below.</p>

<h2 class="govuk-heading-l">Consultation JSON</h2>
<p class="govuk-body">The consultation file should describe the consultation conforming to this schema.</p>

<details class="govuk-details">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
JSON schema
</span>
</summary>
<div class="govuk-details__text">
<pre><code>{{ json_schemas['consultation'] }}</code></pre>
</div>
</details>

<details class="govuk-details">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
Example JSON
</span>
</summary>
<div class="govuk-details__text">
<pre><code>{{ json_examples['consultation'] }}</code></pre>
</div>
</details>

<h2 class="govuk-heading-l">ConsultationResponseList JSON</h2>
<p class="govuk-body">The consultation responses file should contain a list of consultation responses conforming to this schema.</p>
<details class="govuk-details">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
JSON schema
</span>
</summary>
<div class="govuk-details__text">
<pre><code>{{ json_schemas['consultation_response_list'] }}</code></pre>
</div>
</details>

<h2 class="govuk-heading-l">Components</h2>
{% for schema in schemas %}
<details class="govuk-details">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
Example JSON
</span>
</summary>
<div class="govuk-details__text">
<pre><code>{{ json_examples['consultation_response_list'] }}</code></pre>
</div>
</details>


<h2 class="govuk-heading-l">Entities</h2>
{% for schema in entity_schemas %}
<h2 class="govuk-heading-m">{{ schema.name() }}</h2>
<p class="govuk-body">{{ schema.description() }}</p>
{% if schema.rows() %}
Expand All @@ -21,30 +67,22 @@ <h2 class="govuk-heading-m">{{ schema.name() }}</h2>
<tr class="govuk-table__row">
<th style="width:30%" scope="col" class="govuk-table__header">Field</th>
<th style="width:30%" scope="col" class="govuk-table__header">Description</th>
<th style="width:40%" scope="col" class="govuk-table__header">Example</th>
</tr>
</thead>
<tbody class="govuk-table__body">
{% for row in schema.rows() %}
<tr class="govuk-table__row">
<th scope="row" class="govuk-table__header">{{ row['name'] }}</th>
<td class="govuk-table__cell">{{ row['description'] }}</td>
<td class="govuk-table__cell"><code>{{ row['example'] }}</code></td>
</tr>
{% endfor %}
</tbody>
</table>
<details class="govuk-details">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
Example JSON
</span>
</summary>
<div class="govuk-details__text">
<pre><code>{{ schema.example() }}</code></pre>
</div>
</details>
{% endif %}
{% endfor %}

<h2 class="govuk-heading-l">Changelog</h2>
<h3 class="govuk-heading-s">28 March 2024</h3>
<p class="govuk-body">Initial release</p>
{% endblock %}

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import json
from django.core.management import BaseCommand

from consultation_analyser.consultations import public_schema


class Command(BaseCommand):
help = "Generates an Entity Relationship Diagram for the repository’s README"

def handle(self, *args, **options):
schema_folder = "./consultation_analyser/consultations/public_schema"

schema = public_schema.Consultation.model_json_schema()
with open(f"{schema_folder}/consultation_schema.json", "w") as f:
json.dump(schema, f)

schema = public_schema.ConsultationResponseList.model_json_schema()
with open(f"{schema_folder}/consultation_response_list_schema.json", "w") as f:
json.dump(schema, f)
103 changes: 82 additions & 21 deletions consultation_analyser/consultations/public_schema.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,119 @@
# generated by datamodel-codegen:
# filename: public_schema.yaml
# timestamp: 2024-03-27T09:03:22+00:00
# timestamp: 2024-03-28T07:59:40+00:00

from __future__ import annotations

from typing import List, Optional
from uuid import UUID

from pydantic import BaseModel, Field
from pydantic import BaseModel, Extra, Field


class Question(BaseModel):
id: Optional[UUID] = None
text: Optional[str] = Field(
None,
"""
Questions can be free text, multiple choice or both. The presence of multiple_choice_options implies that the question has a multiple choice part.
"""

id: UUID = Field(..., description='The ID of this question')
text: str = Field(
...,
description='The question text',
example='Should Kit Kats be banned on Tuesdays',
examples=[
'Should it happen on Tuesdays?',
'Should it happen in the month of May?',
'Should it happen on a full moon?',
'Should it happen on Fridays?',
'Should it be forbidden on Sunday?',
],
)
has_free_text: Optional[bool] = Field(
None, description='Does this question have a free text component?'
has_free_text: bool = Field(
..., description='Does this question have a free text component?'
)
multiple_choice_options: Optional[List[str]] = Field(
None,
description='The options for the multiple choice part of this question, if it has a multiple choice component',
example=['Yes', 'No', "I don't know"],
)


class Answer(BaseModel):
multiple_choice: Optional[List[str]] = Field(
None,
"""
Each Answer is associated with a Question and belongs to a ConsultationResponse.
"""

question_id: UUID = Field(..., description='The ID of the question')
multiple_choice: List[str] = Field(
...,
description='Responses to the multiple choice part of the question, if any',
example=['No'],
)
free_text: Optional[str] = Field(
None, description='The answer to the free text part of the question, if any'
free_text: str = Field(
...,
description='The answer to the free text part of the question, if any',
examples=[
"I don't think this is a good idea at all",
'I would like to point out a few things',
'I would like clarification on a few key points',
],
)


class ConsultationResponse(BaseModel):
answers: Optional[List[Answer]] = None
"""
A ConsultationResponse groups answers. For now it is also a placeholder for response-level information such as demographics, responding-in-the-capacity-of, etc.
"""

id: UUID = Field(..., description='The ID of the response')
answers: List[Answer] = Field(..., description='The answers in this response')


class ConsultationResponseList(BaseModel):
"""
A list of ConsultationResponses
"""

consultation_responses: List[ConsultationResponse] = Field(
..., description='The responses'
)


class Section(BaseModel):
name: Optional[str] = Field(
None,
"""
A Section contains a group of Questions. Consultations that do not have multiple sections should group all Questions under a single Section.
"""

id: UUID = Field(..., description='The ID of the section')
name: str = Field(
...,
description='The name of the section',
example='When to enforce a Kit Kat ban',
examples=[
'When to enforce a Kit Kat ban',
'When to encourage the eating of Kit Kats',
'When Kit Kats are consumed',
],
)
questions: List[Question] = Field(
..., description='The questions in the consultation'
)
questions: Optional[List[Question]] = None


class Consultation(BaseModel):
name: Optional[str] = Field(
None,
"""
Consultation is the top-level object describing a consultation. It contains one or more Sections, which in turn contain Questions.
"""

class Config:
extra = Extra.forbid

name: str = Field(
...,
description='The name of the consultation',
example='Consultation on Kit Kats',
examples=[
'Consultation on Kit Kats',
'How should Kit Kats change',
'What shall we do about Kit Kats',
],
)
sections: Optional[List[Section]] = None
sections: List[Section] = Field(..., description='The sections of the consultation')
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"Consultation on Kit Kats","sections":[{"id":"cf887a0f-c0ed-3cc8-20df-15cac2582f9d","name":"When to encourage the eating of Kit Kats","questions":[{"id":"75d33882-1c6f-f348-2ecb-bb995a0c7772","text":"Should it happen on Tuesdays?","has_free_text":false},{"id":"e54b60ae-a6f5-19ee-6410-6441a9d829bb","text":"Should it happen on a full moon?","has_free_text":false},{"id":"da7af605-fd4e-41ec-b276-54e24aa64081","text":"Should it be forbidden on Sunday?","has_free_text":false},{"id":"680f044d-a417-06a9-648b-e5884353d9f4","text":"Should it happen on Tuesdays?","has_free_text":true},{"id":"efb96cf3-e455-66d5-ae8e-e1f22cd236c3","text":"Should it happen on a full moon?","has_free_text":false}]},{"id":"6c48808e-9198-3fe3-63e9-a8319e5e2ab5","name":"When to encourage the eating of Kit Kats","questions":[{"id":"fa5cd756-92b2-71a9-9584-61da063d48b4","text":"Should it happen in the month of May?","has_free_text":true,"multiple_choice_options":["Yes","No","I don't know"]},{"id":"c511a280-15ea-b963-c482-9d6fb568a31c","text":"Should it happen on Tuesdays?","has_free_text":true},{"id":"843b764e-cfb5-ca99-28c5-9000a266f798","text":"Should it happen on Fridays?","has_free_text":false},{"id":"7c9c063c-2b4c-3e60-7d20-259b51707a63","text":"Should it happen on Fridays?","has_free_text":false},{"id":"32ca1a40-1d44-328c-4e63-025bb97210b2","text":"Should it be forbidden on Sunday?","has_free_text":false}]}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"consultation_responses":[{"id":"a0f5cd91-4e2e-0f78-d0e4-f209a15fec70","answers":[{"question_id":"c811d9b9-5ade-5917-515c-d1d7a6d3a3e8","multiple_choice":["No"],"free_text":"I would like to point out a few things"},{"question_id":"6622f17c-48e1-fcc6-6534-c6fcbf345cee","multiple_choice":["No"],"free_text":"I would like to point out a few things"},{"question_id":"e60c8d99-d764-96f1-5311-f6d0fde01f6b","multiple_choice":["No"],"free_text":"I would like to point out a few things"}]},{"id":"775fc2c1-43d5-030a-d3db-bc00676b7211","answers":[{"question_id":"fd29c40a-84d8-0a6e-c102-86046b489591","multiple_choice":["No"],"free_text":"I would like clarification on a few key points"},{"question_id":"57625272-8c49-d966-9a65-13260679e690","multiple_choice":["No"],"free_text":"I would like to point out a few things"},{"question_id":"dabfa87a-6d85-b0d5-c93e-db31223e1e35","multiple_choice":["No"],"free_text":"I would like to point out a few things"},{"question_id":"b2db7f98-066e-bac7-454a-ccaa29d0a4b1","multiple_choice":["No"],"free_text":"I don't think this is a good idea at all"},{"question_id":"60d64eaf-552e-e58d-474c-b417d367bc03","multiple_choice":["No"],"free_text":"I would like to point out a few things"}]}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$defs": {"Answer": {"description": "Each Answer is associated with a Question and belongs to a ConsultationResponse.", "properties": {"question_id": {"description": "The ID of the question", "format": "uuid", "title": "Question Id", "type": "string"}, "multiple_choice": {"description": "Responses to the multiple choice part of the question, if any", "example": ["No"], "items": {"type": "string"}, "title": "Multiple Choice", "type": "array"}, "free_text": {"description": "The answer to the free text part of the question, if any", "examples": ["I don't think this is a good idea at all", "I would like to point out a few things", "I would like clarification on a few key points"], "title": "Free Text", "type": "string"}}, "required": ["question_id", "multiple_choice", "free_text"], "title": "Answer", "type": "object"}, "ConsultationResponse": {"description": "A ConsultationResponse groups answers. For now it is also a placeholder for response-level information such as demographics, responding-in-the-capacity-of, etc.", "properties": {"id": {"description": "The ID of the response", "format": "uuid", "title": "Id", "type": "string"}, "answers": {"description": "The answers in this response", "items": {"$ref": "#/$defs/Answer"}, "title": "Answers", "type": "array"}}, "required": ["id", "answers"], "title": "ConsultationResponse", "type": "object"}}, "description": "A list of ConsultationResponses", "properties": {"consultation_responses": {"description": "The responses", "items": {"$ref": "#/$defs/ConsultationResponse"}, "title": "Consultation Responses", "type": "array"}}, "required": ["consultation_responses"], "title": "ConsultationResponseList", "type": "object"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$defs": {"Question": {"description": "Questions can be free text, multiple choice or both. The presence of multiple_choice_options implies that the question has a multiple choice part.", "properties": {"id": {"description": "The ID of this question", "format": "uuid", "title": "Id", "type": "string"}, "text": {"description": "The question text", "examples": ["Should it happen on Tuesdays?", "Should it happen in the month of May?", "Should it happen on a full moon?", "Should it happen on Fridays?", "Should it be forbidden on Sunday?"], "title": "Text", "type": "string"}, "has_free_text": {"description": "Does this question have a free text component?", "title": "Has Free Text", "type": "boolean"}, "multiple_choice_options": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "default": null, "description": "The options for the multiple choice part of this question, if it has a multiple choice component", "example": ["Yes", "No", "I don't know"], "title": "Multiple Choice Options"}}, "required": ["id", "text", "has_free_text"], "title": "Question", "type": "object"}, "Section": {"description": "A Section contains a group of Questions. Consultations that do not have multiple sections should group all Questions under a single Section.", "properties": {"id": {"description": "The ID of the section", "format": "uuid", "title": "Id", "type": "string"}, "name": {"description": "The name of the section", "examples": ["When to enforce a Kit Kat ban", "When to encourage the eating of Kit Kats", "When Kit Kats are consumed"], "title": "Name", "type": "string"}, "questions": {"description": "The questions in the consultation", "items": {"$ref": "#/$defs/Question"}, "title": "Questions", "type": "array"}}, "required": ["id", "name", "questions"], "title": "Section", "type": "object"}}, "additionalProperties": false, "description": "Consultation is the top-level object describing a consultation. It contains one or more Sections, which in turn contain Questions.", "properties": {"name": {"description": "The name of the consultation", "examples": ["Consultation on Kit Kats", "How should Kit Kats change", "What shall we do about Kit Kats"], "title": "Name", "type": "string"}, "sections": {"description": "The sections of the consultation", "items": {"$ref": "#/$defs/Section"}, "title": "Sections", "type": "array"}}, "required": ["name", "sections"], "title": "Consultation", "type": "object"}
67 changes: 0 additions & 67 deletions consultation_analyser/consultations/public_schemas.py

This file was deleted.

Loading

0 comments on commit a6fe88b

Please sign in to comment.