parent
f66d5e9e25
commit
a5543e0099
BIN
.doit.db.db
BIN
.doit.db.db
Binary file not shown.
|
@ -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 %}}
|
|
@ -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.
|
|
@ -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
|
|
@ -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]
|
|
@ -0,0 +1,2 @@
|
||||||
|
Nextcloud::https://nextcloud.com/install
|
||||||
|
Nextcloud Forms App::https://apps.nextcloud.com/apps/forms
|
|
@ -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;
|
||||||
|
})();
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue