Nikola auto commit.

Nikola version: 8.3.3
src
Patricio García 2025-08-15 13:55:02 +01:00
parent f66d5e9e25
commit a5543e0099
9 changed files with 428 additions and 0 deletions

Binary file not shown.

14
pages/inscription.md Normal file
View File

@ -0,0 +1,14 @@
<!--
.. title: Formulario de inscripción
.. slug: inscription
.. date: 2025-08-15 11:55:30 UTC+01:00
.. tags:
.. category:
.. link:
.. description: Formulario de inscripción en EDUCATIC
.. type: text
-->
{{% nextcloud_forms link="https://nube.txs.es/apps/forms/s/BPYqzqF44bFCXb3H7eFe8888" %}}
¡Muchas gracias! En breve nos pondremos en contacto contigo
{{% /nextcloud_forms %}}

View File

@ -0,0 +1,40 @@
This plugin embeds a Nextcloud Forms formular in a static site.
It provides a mechanism for the static HTML site to embed a form and to collect
and store the user data.
The form specification is loaded at build time from the Nextcloud using the
public form link. The HTML form is then build according to the extracted JSON
form specification.
When the submit button is pressed a simple JS function collects the form data
and sends it to the Nextcloud server using the public API.
## Requirements and setup
For this plugin to work a Nextcloud with the Forms App is needed.
Create a form and make it public. Copy the public link and pass it as the
`link` parameter to the nextcloud_forms shortcut:
```
{{% nextcloud_forms link="https://nextcloud_url/index.php/apps/forms/..." %}}
Success Text
{{% /nextcloud_forms %}}
```
The `Success Text` is shown after the form is successfully sent to the server.
## Heads up
Nextcloud Forms public API is not handling CORS requests correctly, especially
the preflight checks. This leads to the XmlHTTPRequest failing with a CORS
error and the browser not submitting the data.
At the moment there are two open feature requests at Nextcloud Server and
Nextcloud Forms that should fix these issues:
- https://github.com/nextcloud/server/pull/31698
- https://github.com/nextcloud/forms/pull/1139
Until these are merged into upstream, at least the changes to Nextcloud Forms
have to be applied manually to the Nextcloud installation.

View File

@ -0,0 +1,12 @@
[Core]
name = nextcloud_forms
module = nextcloud_forms
[Nikola]
PluginCategory = Shortcode
[Documentation]
author = Andreas Brinner
version = 0.1
website = https://getnikola.com/
description = Embed Nextcloud Forms

View File

@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
# Copyright © 2022, Andreas Brinner.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
# documentation files (the "Software"), to deal in the
# Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the
# Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice
# shall be included in all copies or substantial portions of
# the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import os
import json
import base64
import requests
from html.parser import HTMLParser
from urllib.parse import urlparse
from nikola.plugin_categories import ShortcodePlugin
SUBMIT_FORM_JS_PATH = os.path.join(os.path.dirname(__file__), 'submit_form.js')
class FormDataParser(HTMLParser):
def __init__(self, *args, **argv):
self.data = {}
HTMLParser.__init__(self, *args, **argv)
def handle_starttag(self, tag, attrs):
if tag == "input":
data = dict(attrs)
if "id" in data and "value" in data:
self.data[data["id"]] = json.loads(base64.b64decode(data["value"]))
class Plugin(ShortcodePlugin):
"""Plugin for nextcloud_forms directive."""
name = "nextcloud_forms"
def get_public_form_data(self, link):
try:
data = requests.get(link).text
except requests.exceptions.RequestException:
self.logger.error('Cannot get form data from url={0}', link)
parser = FormDataParser()
parser.feed(data)
return parser.data
def generate_ocs_url(self, link):
"""Generate the OCS API base url from the given link.
see also:
https://github.com/nextcloud/nextcloud-router/blob/master/lib/index.ts#L29"
"""
url = urlparse(link)
return "{}://{}/ocs/v2.php/apps/forms".format(url.scheme, url.netloc)
def set_site(self, site):
super(type(self), self).set_site(site)
with open(SUBMIT_FORM_JS_PATH, "r") as fd:
submit_form_js = "<script>{}</script>".format(fd.read())
site.template_hooks['body_end'].append(submit_form_js)
def handler(self, link, template="nextcloud_forms.tmpl",
site=None, data=None, lang=None, post=None, **argv):
"""Create HTML for Nextcloud Forms formular."""
form_data = self.get_public_form_data(link)
endpoint = self.generate_ocs_url(link)
endpoint += "/api/v1.1/submission/insert"
form = form_data.get("initial-state-forms-form")
if not form:
self.logger.error('No form defintion fond in url={}'.format(link))
template_deps = site.template_system.template_deps(template, site.GLOBAL_CONTEXT)
template_data = site.GLOBAL_CONTEXT.copy()
template_data.update({
'lang': lang,
'form': form,
'endpoint': endpoint,
'initial_state': form_data,
'success_data': data,
})
output = site.template_system.render_template(
template, None, template_data)
return output, template_deps + [__file__, SUBMIT_FORM_JS_PATH]

View File

@ -0,0 +1,2 @@
Nextcloud::https://nextcloud.com/install
Nextcloud Forms App::https://apps.nextcloud.com/apps/forms

View File

@ -0,0 +1,69 @@
(function () {
function submit(ev) {
const form = ev.target;
ev.preventDefault();
const answers = {};
const [,formId] = form.id.split("-");
for (let i = 0; i < form.length; i++) {
var input = form[i];
// extract question id (qid) and answer id (aid)
var [,,qid,,aid] = input.id.split("-");
if ( qid && !(qid in answers) )
answers[qid] = [];
switch (input.type) {
case "checkbox":
case "radio":
if (input.checked)
answers[qid].push(aid);
console.log(input.type, input.id, qid, aid, input.checked);
break;
case "select-one":
case "text":
case "textarea":
case "date":
case "datetime-local":
if (input.value)
answers[qid].push(input.value);
console.log(input.type, input.id, qid, input.value);
break;
default:
console.log("unknown form element type:", input.type);
}
}
const request = new XMLHttpRequest();
request.open("POST", form.action, true);
request.setRequestHeader("OCS-APIRequest", "true");
request.setRequestHeader("Accept", "application/json");
request.setRequestHeader("Content-Type", "application/json");
request.onload = function () {
const message = document.getElementById("form-" + formId + "-messages");
const form = document.getElementById("form-" + formId);
const success = document.getElementById("form-" + formId + "-success");
const response = JSON.parse(this.response);
if (this.status == 200) {
// success
form.style.display = "none";
success.style.display = "block";
} else {
message.innerHTML = '<div class="alert alert-danger" role="alert">' +
this.statusText + "(" + this.status + "): " +
response['ocs']['meta']['message'] +
'</div>';
}
};
request.send(JSON.stringify({'formId': formId, 'answers': answers}));
}
for (let i = 0; i < document.forms.length; i++)
document.forms[i].onsubmit = submit;
})();

View File

@ -0,0 +1,79 @@
<form action="{{ endpoint }}" id="form-{{ form.id }}">
<div id="form-{{ form.id }}-messages"></div>
{% for question in form.questions %}
<div class="mb-3">
<label for="{{ question.formId }}-question-{{question.id}}"
class="form-label">{{ question.text }}{{ " *" if question.isRequired }}</label>
{% if question.type == "short" %}
<input type="text" class="form-control"
id="{{ question.formId }}-question-{{question.id}}"
aria-label="Eine kurze Antwort zu Frage „{{ question.text }}“"
placeholder="Kurze Antwort eingeben"
maxlength="4096" minlength="1" {{ "required" if question.isRequired }}>
{% elif question.type == "long" %}
<textarea class="form-control"
id="{{ question.formId }}-question-{{question.id}}"
aria-label="Eine lange Antwort zu Frage „{{ question.text }}“"
placeholder="Einen langen Text eingeben"
maxlength="4096" minlength="1" {{ "required" if question.isRequired }}
rows="3">
</textarea>
{% elif question.type == "multiple" %}
{% for option in question.options %}
<div class="form-check">
<input class="form-check-input" type="checkbox" value=""
id="{{ question.formId }}-question-{{ question.id }}-answer-{{ option.id }}">
<label class="form-check-label"
for="{{ question.formId }}-question-{{ question.id }}-answer-{{ option.id }}">
{{ option.text }}
</label>
</div>
{% endfor %}
{% elif question.type == "multiple_unique" %}
{% for option in question.options %}
<div class="form-check">
<input class="form-check-input" type="radio" {{ "required" if question.isRequired }}
name="{{ question.formId }}-question-{{ question.id }}"
id="{{ question.formId }}-question-{{ question.id }}-answer-{{ option.id }}">
<label class="form-check-label"
for="{{ question.formId }}-question-{{ question.id }}-answer-{{ option.id }}">
{{ option.text }}
</label>
</div>
{% endfor %}
{% elif question.type == "dropdown" %}
<select class="form-select" {{ "required" if question.isRequired }}
name="{{ question.text }}"
id="{{ question.formId }}-question-{{ question.id }}">
<option value=""> Wählen Sie eine Option </option>
{% for option in question.options %}
<option value="{{ option.id }}"
id="{{ question.formId }}-question-{{ question.id }}-answer-{{ option.id }}">
{{ option.text }}</option>
{% endfor %}
</select>
{% elif question.type == "date" %}
<input class="form-control" type="date" placeholder="Datum auswählen"
{{ "required" if question.isRequired }}
id="{{ question.formId }}-question-{{ question.id }}">
{% elif question.type == "datetime" %}
<input class="form-control" type="datetime-local"
{{ "required" if question.isRequired }}
placeholder="Datum und Uhrzeit auswählen"
id="{{ question.formId }}-question-{{ question.id }}">
{% else %}
<div class="alert alert-danger" role="alert">
Unbekannter Frage Typ (question.type: {{ question.type }})
</div>
{% endif %}
</div>
{% endfor %}
<button type="submit" aria-label="Formular übermitteln"
class="btn btn-primary">Übermitteln
</button>
</form>
<div style="display: none;" id="form-{{ form.id }}-success">
{{ success_data if success_data else "Success" }}
</div>

View File

@ -0,0 +1,103 @@
<form action="${endpoint}" id="form-${form['id']}">
<div id="form-${form['id']}-messages"></div>
% for question in form['questions']:
<div class="mb-3">
<label for="${question['formId'] }-question-${question['id']}"
class="form-label">${question['text']}
% if question['isRequired']:
*
% endif
</label>
% if question['type'] == "short":
<input type="text" class="form-control"
id="${question['formId'] }-question-${question['id']}"
aria-label="A short answer to question '${question['text']}'"
placeholder="Enter a short answer"
maxlength="4096" minlength="1"
% if question['isRequired']:
required
% endif
% elif question['type'] == "long":
<textarea class="form-control"
id="${question['formId']}-question-${question['id']}"
aria-label="A long answer to the question '${question['text']}'"
placeholder="Enter a long text"
maxlength="4096" minlength="1"
% if question['isRequired']:
required
% endif
rows="3">
</textarea>
% elif question['type'] == "multiple":
% for option in question['options']:
<div class="form-check">
<input class="form-check-input" type="checkbox" value=""
id="${question['formId']}-question-${question['id']}-answer-${option['id']}">
<label class="form-check-label"
for="${question['formId']}-question-${question['id']}-answer-${option['id']}">
${option['text']}
</label>
</div>
% endfor
% elif question['type'] == "multiple_unique":
% for option in question['options']:
<div class="form-check">
<input class="form-check-input" type="radio"
% if question['isRequired']:
required
% endif
name="${question['formId']}-question-${question['id']}"
id="${question['formId']}-question-${question['id']}-answer-${option['id']}">
<label class="form-check-label"
for="${question['formId']}-question-${question['id']}-answer-${option['id']}">
${option['text']}
</label>
</div>
% endfor
% elif question['type'] == "dropdown":
<select class="form-select"
% if question['isRequired']:
required
% endif
name="${question['text']}"
id="${question['formId']}-question-${question['id']}">
<option value=""> Select an option</option>
% for option in question['options']:
<option value="${option['id']}"
id="${question['formId']}-question-${question['id']}-answer-${option['id']}">
${option['text']}</option>
% endfor
</select>
% elif question['type'] == "date":
<input class="form-control" type="date" placeholder="Select a date"
% if question['isRequired']:
required
% endif
id="${question['formId']}-question-${question['id']}">
% elif question['type'] == "datetime":
<input class="form-control" type="datetime-local"
% if question['isRequired']:
required
% endif
placeholder="Select a date and time"
id="${question['formId']}-question-${question['id']}">
% else:
<div class="alert alert-danger" role="alert">
Unknown question type (question.type: ${question['type']})
</div>
% endif
</div>
% endfor
<button type="submit" aria-label="Submit the form"
class="btn btn-primary">Submit
</button>
</form>
<div style="display: none;" id="form-${form['id']}-success">
% if success_data:
${success_data}
% else:
Success
% endif
</div>