From befc284184bb0b9a91d03e9ec5143af707e13a3f Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Thu, 18 Apr 2019 22:23:08 +0200
Subject: [PATCH] Added basic HTML generator from schema

---
 retribute_me/cli.py                   | 53 +++++++++++++++++++++-
 retribute_me/models.py                | 28 ++++++++++++
 retribute_me/templates/profile.jinja2 | 64 +++++++++++++++++++++++++++
 schemas/0.1/example.json              |  2 +
 schemas/0.1/schema.json               | 27 ++++++++---
 5 files changed, 167 insertions(+), 7 deletions(-)
 create mode 100644 retribute_me/models.py
 create mode 100644 retribute_me/templates/profile.jinja2

diff --git a/retribute_me/cli.py b/retribute_me/cli.py
index 06cbdb5..7a407c2 100644
--- a/retribute_me/cli.py
+++ b/retribute_me/cli.py
@@ -1,9 +1,16 @@
 import click
+import jinja2
 import json
 import jsonschema
+import os
+
+from . import models
 from . import schemas
 
 
+PROJECT_DIR = os.path.abspath(os.path.dirname(__file__))
+
+
 @click.group()
 def cli():
     pass
@@ -22,9 +29,51 @@ def validate(document, version):
         except KeyError:
             version = schemas.latest_schema_version
 
-    schema = schemas.get(version)
+    try:
+        schema = schemas.get(version)
+    except FileNotFoundError:
+        raise click.ClickException("{} is not a valid version".format(version))
     jsonschema.validate(instance=payload, schema=schema)
-    click.echo("Schema is valid!")
+    click.echo("Document is valid!")
+
+
+def get_template_context(document):
+    context = {"doc": document, "version": document["version"]}
+    context["sorted_means"] = [
+        models.Mean(**mean_data)
+        for mean_data in sorted(
+            document.get("means", []), key=lambda v: v["weight"], reverse=True
+        )
+    ]
+    context["sorted_identities"] = [
+        models.Identity(**identity_data)
+        for identity_data in sorted(
+            document.get("identities", []), key=lambda v: v["weight"], reverse=True
+        )
+    ]
+    return context
+
+
+@cli.command()
+@click.argument("document", type=click.File("r"))
+@click.argument("output", type=click.File("w"))
+@click.option("--template", "-t", type=click.File("r"))
+def html(document, output, template):
+    content = document.read()
+    document = json.loads(content)
+    context = get_template_context(document)
+    if not template:
+        default_template = os.path.join(PROJECT_DIR, "templates", "profile.jinja2")
+        with open(default_template, "r") as f:
+            template_content = f.read()
+    else:
+        template_content = template.read()
+
+    t = jinja2.Template(
+        "{% autoescape true %}" + template_content + "{% endautoescape %}",
+        undefined=jinja2.StrictUndefined,
+    )
+    click.echo(t.render(**context))
 
 
 if __name__ == "__main__":
diff --git a/retribute_me/models.py b/retribute_me/models.py
new file mode 100644
index 0000000..1590140
--- /dev/null
+++ b/retribute_me/models.py
@@ -0,0 +1,28 @@
+class Identity(object):
+    def __init__(self, provider, id, weight, name=None, summary=None, url=None):
+        self.provider = provider
+        self.id = id
+        self.summary = summary
+        self.weight = weight
+        self.name = name
+        self.url = url
+
+    def get_full_name(self):
+        if self.name:
+            return self.name
+        return "{} on {}".format(self.id, self.provider)
+
+
+class Mean(object):
+    def __init__(self, provider, id, weight, name=None, summary=None, url=None):
+        self.provider = provider
+        self.id = id
+        self.summary = summary
+        self.weight = weight
+        self.name = name
+        self.url = url
+
+    def get_full_name(self):
+        if self.name:
+            return self.name
+        return "{} on {}".format(self.id, self.provider)
diff --git a/retribute_me/templates/profile.jinja2 b/retribute_me/templates/profile.jinja2
new file mode 100644
index 0000000..c80aa43
--- /dev/null
+++ b/retribute_me/templates/profile.jinja2
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <title>{{ doc['title'] }}</title>
+    {%- if "summary" in doc %}
+    <meta name="description" content="{{ doc['summary'] }}" />
+    {% endif %}
+  </head>
+  <body>
+    <header>
+      <h1>{{ doc['title'] }}</h1>
+      {%- if "summary" in doc %}
+      <p>{{ doc['summary'] }}</p>
+      {% endif %}
+    </header>
+    <main>
+      {%- if sorted_identities|length > 0 %}
+      <section class="identity">
+        <h2>My other identities</h2>
+        {% for identity in sorted_identities %}
+        <article>
+          <h3>
+            {%- if identity.url %}
+            <a href="{{ identity.url }}" rel="me noopener">
+              {{ identity.get_full_name() }}
+            </a>
+            {%- else %}
+              {{ identity.get_full_name() }}
+            {%- endif %}
+          </h3>
+          {%- if identity.summary %}
+          <p>{{ identity.summary }}</p>
+          {%- endif %}
+        </article>
+        {% endfor %}
+      </section>
+      {%- endif %}
+      {%- if sorted_means|length > 0 %}
+      <section>
+        <h2>Support me!</h2>
+        {% for mean in sorted_means %}
+        <article class="mean">
+          <h3>
+            {%- if mean.url %}
+            <a href="{{ mean.url }}" rel="me noopener">
+              {{ mean.get_full_name() }}
+            </a>
+            {%- else %}
+              {{ mean.get_full_name() }}
+            {%- endif %}
+          </h3>
+          {%- if mean.summary %}
+          <p>{{ mean.summary }}</p>
+          {%- endif %}
+        </article>
+        {% endfor %}
+      </section>
+      {%- endif %}
+    </main>
+  </body>
+</html>
diff --git a/schemas/0.1/example.json b/schemas/0.1/example.json
index dc7ed6d..f30ef32 100644
--- a/schemas/0.1/example.json
+++ b/schemas/0.1/example.json
@@ -2,6 +2,7 @@
   "version": "0.1",
   "id": "https://retribute.me/@Alice.json",
   "url": "https://retribute.me/@Alice.html",
+  "title": "Caring is loving",
   "summary": "My name is Alice, I produce Creative Commons electro music. I need your support to continue working on my audio pieces!",
   "identities": [
     {
@@ -19,6 +20,7 @@
     {
       "provider": "custom",
       "id": "personal-website",
+      "name": "My personal website",
       "url": "https://alice.me",
       "weight": 3
     }
diff --git a/schemas/0.1/schema.json b/schemas/0.1/schema.json
index 37460a3..c901a95 100644
--- a/schemas/0.1/schema.json
+++ b/schemas/0.1/schema.json
@@ -6,6 +6,7 @@
   "additionalProperties": false,
   "required": [
     "id",
+    "title",
     "version",
     "means"
   ],
@@ -30,26 +31,38 @@
         "https://retribute.me/@Alice.html"
       ]
     },
+    "title": {
+      "description": "A human readable title to go with the profile",
+      "type": "string"
+    },
     "summary": {
-      "description": "A human readable exaplanation to go with the profile",
+      "description": "A human readable explanation to go with the profile",
       "type": "string"
     },
     "means": {
       "description": "A list of retribution means",
       "type": "array",
-      "items": { "$ref": "#/definitions/mean" }
+      "items": {
+        "$ref": "#/definitions/mean"
+      }
     },
     "identities": {
       "description": "A list of identities lined to the profile",
       "type": "array",
-      "items": { "$ref": "#/definitions/identity" }
+      "items": {
+        "$ref": "#/definitions/identity"
+      }
     }
   },
   "definitions": {
     "identity": {
       "description": "A single retribution mean. The provider and id must form a unique combination",
       "type": "object",
-      "required": ["provider", "id", "weight"],
+      "required": [
+        "provider",
+        "id",
+        "weight"
+      ],
       "minItems": 0,
       "properties": {
         "provider": {
@@ -89,7 +102,11 @@
     "mean": {
       "description": "A single retribution mean. The provider and id must form a unique combination",
       "type": "object",
-      "required": ["provider", "id", "weight"],
+      "required": [
+        "provider",
+        "id",
+        "weight"
+      ],
       "minItems": 1,
       "properties": {
         "provider": {
-- 
GitLab