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