Feat: Replace Google ReCAPTCHA with ALTCHA using API - non-tracking, GDPR compliant without cookies or fingerprinting.

This commit is contained in:
2025-03-13 15:36:41 +00:00
parent 29205de12f
commit b843849af9
12 changed files with 2856 additions and 722 deletions

View File

@@ -81,9 +81,15 @@ class Config:
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD') MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER') MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
MAIL_CONTACT_PUBLIC = os.getenv('MAIL_CONTACT_PUBLIC') MAIL_CONTACT_PUBLIC = os.getenv('MAIL_CONTACT_PUBLIC')
"""
# Recaptcha # Recaptcha
RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY') RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY')
RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY') RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY')
"""
# ALTCHA
ALTCHA_API_KEY = os.getenv('ALTCHA_API_KEY')
ALTCHA_SECRET_KEY = os.getenv('ALTCHA_SECRET_KEY')
ALTCHA_REGION = 'eu'
class DevelopmentConfig(Config): class DevelopmentConfig(Config):
is_development = True is_development = True

View File

@@ -18,14 +18,17 @@ from models.model_view_contact import Model_View_Contact
from models.model_view_home import Model_View_Home from models.model_view_home import Model_View_Home
import lib.argument_validation as av import lib.argument_validation as av
# external # external
from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session, Blueprint, current_app from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session, Blueprint, current_app, flash
from flask_mail import Mail, Message from flask_mail import Mail, Message
from extensions import db, oauth, mail from extensions import db, oauth, mail
from urllib.parse import quote_plus, urlencode from urllib.parse import quote_plus, urlencode
from authlib.integrations.flask_client import OAuth from authlib.integrations.flask_client import OAuth
from authlib.integrations.base_client import OAuthError from authlib.integrations.base_client import OAuthError
from urllib.parse import quote, urlparse, parse_qs from urllib.parse import quote, urlparse, parse_qs
import json
import base64
import hmac
import hashlib
routes_core = Blueprint('routes_core', __name__) routes_core = Blueprint('routes_core', __name__)
@@ -54,20 +57,82 @@ def contact_post():
try: try:
form = Form_Contact() form = Form_Contact()
if form.validate_on_submit(): if form.validate_on_submit():
# Handle form submission altcha_payload = form.altcha.data
email = form.email.data if not altcha_payload:
CC = form.CC.data # not in use flash('Please complete the ALTCHA challenge', 'danger')
contact_name = form.contact_name.data return "Invalid. ALTCHA challenge failed."
company_name = form.company_name.data # Decode and verify the ALTCHA payload
message = form.message.data try:
receive_marketing = form.receive_marketing.data decoded_payload = json.loads(base64.b64decode(altcha_payload))
receive_marketing_text = "I would like to receive marketing emails." if receive_marketing else ""
# send email # Verify the signature
mailItem = Message("PARTS Website Contact Us Message", recipients=[current_app.config['MAIL_CONTACT_PUBLIC']]) if verify_altcha_signature(decoded_payload):
mailItem.body = f"Dear Lord Edward Middleton-Smith,\n\n{message}\n{receive_marketing_text}\nKind regards,\n{contact_name}\n{company_name}\n{email}" # Parse the verification data
mail.send(mailItem) verification_data = urllib.parse.parse_qs(decoded_payload['verificationData'])
return "Submitted."
# Check if the verification was successful
if verification_data.get('verified', ['false'])[0] == 'true':
# If spam filter is enabled, check the classification
if 'classification' in verification_data:
classification = verification_data.get('classification', [''])[0]
score = float(verification_data.get('score', ['0'])[0])
# If the classification is BAD and score is high, reject the submission
if classification == 'BAD' and score > 5:
flash('Your submission was flagged as potential spam', 'error')
return render_template('contact.html', form=form)
# Process the form submission
email = form.email.data
CC = form.CC.data # not in use
contact_name = form.contact_name.data
company_name = form.company_name.data
message = form.message.data
receive_marketing = form.receive_marketing.data
receive_marketing_text = "I would like to receive marketing emails." if receive_marketing else ""
# send email
mailItem = Message("PARTS Website Contact Us Message", recipients=[current_app.config['MAIL_CONTACT_PUBLIC']])
mailItem.body = f"Dear Lord Edward Middleton-Smith,\n\n{message}\n{receive_marketing_text}\nKind regards,\n{contact_name}\n{company_name}\n{email}"
mail.send(mailItem)
flash('Thank you for your message. We will get back to you soon!', 'success')
return "Submitted."
else:
flash('CAPTCHA verification failed', 'error')
else:
flash('Invalid verification signature', 'error')
except Exception as e:
flash(f'Error verifying CAPTCHA: {str(e)}', 'error')
return "Invalid. Failed to submit." return "Invalid. Failed to submit."
# html_body = render_template('pages/core/_contact.html', model = model) # html_body = render_template('pages/core/_contact.html', model = model)
except Exception as e: except Exception as e:
return jsonify(error=str(e)), 403 return jsonify(error=str(e)), 403
def verify_altcha_signature(payload):
"""Verify the ALTCHA signature"""
if 'algorithm' not in payload or 'signature' not in payload or 'verificationData' not in payload:
return False
algorithm = payload['algorithm']
signature = payload['signature']
verification_data = payload['verificationData']
# Calculate SHA hash of the verification data
if algorithm == 'SHA-256':
hash_func = hashlib.sha256
else:
# Fallback to SHA-256 if algorithm not specified
hash_func = hashlib.sha256
# Calculate the hash of verification_data
data_hash = hash_func(verification_data.encode('utf-8')).digest()
# Calculate the HMAC signature
calculated_signature = hmac.new(
current_app.config["ALTCHA_SECRET_KEY"].encode('utf-8'),
data_hash,
hash_func
).hexdigest()
# Compare the calculated signature with the provided signature
return hmac.compare_digest(calculated_signature, signature)

View File

@@ -16,12 +16,63 @@ Defines Flask-WTF form for handling user input on Contact Us page.
# from models.model_view_store import Model_View_Store # circular # from models.model_view_store import Model_View_Store # circular
from forms.base import Form_Base from forms.base import Form_Base
# external # external
from flask import Flask, render_template, request, flash, redirect, url_for, current_app
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField, BooleanField, IntegerField, SelectField, FloatField from wtforms import StringField, TextAreaField, SubmitField, HiddenField, BooleanField
from wtforms.validators import InputRequired, NumberRange, Regexp, DataRequired, Optional from wtforms.validators import DataRequired, Email, ValidationError
from flask_wtf.recaptcha import RecaptchaField from flask_wtf.recaptcha import RecaptchaField
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import requests
import json
import hmac
import hashlib
import base64
import urllib.parse
"""
def validate_altcha(form, field):
if not field.data:
raise ValidationError('Please complete the ALTCHA challenge')
try:
# Decode the base64-encoded payload
payload_json = base64.b64decode(field.data).decode('utf-8')
payload = json.loads(payload_json)
# Verify ALTCHA response
if not payload.get('verified', False):
raise ValidationError('ALTCHA verification failed')
# Verify signature
verification_data = payload.get('verificationData', '')
received_signature = payload.get('signature', '')
algorithm = payload.get('algorithm', 'SHA-256')
# Calculate the hash of verification data
verification_hash = hashlib.sha256(verification_data.encode()).digest()
# Calculate HMAC signature
hmac_key = current_app.config['ALTCHA_SECRET_KEY'].encode()
calculated_signature = hmac.new(
hmac_key,
verification_hash,
getattr(hashlib, algorithm.lower().replace('-', ''))
).hexdigest()
if calculated_signature != received_signature:
raise ValidationError('Invalid ALTCHA signature')
# Optional: If using the spam filter, you could parse verification_data
# and reject submissions classified as spam
# Example:
parsed_data = dict(urllib.parse.parse_qsl(verification_data))
if parsed_data.get('classification') == 'BAD':
raise ValidationError('This submission was classified as spam')
except Exception as e:
current_app.logger.error(f"ALTCHA validation error: {str(e)}")
raise ValidationError('ALTCHA validation failed')
"""
class Form_Contact(FlaskForm): class Form_Contact(FlaskForm):
email = StringField('Email') email = StringField('Email')
@@ -29,5 +80,6 @@ class Form_Contact(FlaskForm):
company_name = StringField('Company') company_name = StringField('Company')
message = TextAreaField('Message') message = TextAreaField('Message')
receive_marketing = BooleanField('I would like to receive marketing emails.') receive_marketing = BooleanField('I would like to receive marketing emails.')
recaptcha = RecaptchaField() # recaptcha = RecaptchaField()
altcha = HiddenField('ALTCHA') # , validators=[validate_altcha]
submit = SubmitField('Send Message') submit = SubmitField('Send Message')

View File

@@ -133,7 +133,7 @@ class Model_View_Base(BaseModel, ABC):
FLAG_PAGE_BODY: ClassVar[str] = 'page-body' FLAG_PAGE_BODY: ClassVar[str] = 'page-body'
FLAG_PHONE_NUMBER: ClassVar[str] = Base.FLAG_PHONE_NUMBER FLAG_PHONE_NUMBER: ClassVar[str] = Base.FLAG_PHONE_NUMBER
FLAG_POSTCODE: ClassVar[str] = Base.FLAG_POSTCODE FLAG_POSTCODE: ClassVar[str] = Base.FLAG_POSTCODE
FLAG_RECAPTCHA: ClassVar[str] = 'recaptcha' FLAG_CAPTCHA: ClassVar[str] = 'recaptcha'
FLAG_RIGHT_HAND_SIDE: ClassVar[str] = 'rhs' FLAG_RIGHT_HAND_SIDE: ClassVar[str] = 'rhs'
FLAG_ROW: ClassVar[str] = 'row' FLAG_ROW: ClassVar[str] = 'row'
FLAG_ROW_NEW: ClassVar[str] = 'row-new' FLAG_ROW_NEW: ClassVar[str] = 'row-new'

View File

@@ -26,6 +26,7 @@ class Model_View_Contact(Model_View_Base):
FLAG_COMPANY_NAME: ClassVar[str] = 'company_name' FLAG_COMPANY_NAME: ClassVar[str] = 'company_name'
FLAG_CONTACT_NAME: ClassVar[str] = 'contact_name' FLAG_CONTACT_NAME: ClassVar[str] = 'contact_name'
FLAG_RECEIVE_MARKETING: ClassVar[str] = 'receive_marketing' FLAG_RECEIVE_MARKETING: ClassVar[str] = 'receive_marketing'
ID_CONTACT_FORM: ClassVar[str] = 'contact-form'
""" """
ID_EMAIL: ClassVar[str] = 'email' ID_EMAIL: ClassVar[str] = 'email'
ID_COMPANY_NAME: ClassVar[str] = 'company_name' ID_COMPANY_NAME: ClassVar[str] = 'company_name'

View File

@@ -13,4 +13,5 @@ python_dotenv
authlib authlib
pydantic pydantic
# psycopg2 # psycopg2
requests requests
cryptography

View File

@@ -1,603 +1,7 @@
/******/ (() => { // webpackBootstrap /******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({ /******/ "use strict";
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
/***/ 431:
/***/ (function(module) {
!function (e, t) {
true ? module.exports = t() : 0;
}(this, function () {
return function (e) {
function t(o) {
if (n[o]) return n[o].exports;
var i = n[o] = {
exports: {},
id: o,
loaded: !1
};
return e[o].call(i.exports, i, i.exports, t), i.loaded = !0, i.exports;
}
var n = {};
return t.m = e, t.c = n, t.p = "dist/", t(0);
}([function (e, t, n) {
"use strict";
function o(e) {
return e && e.__esModule ? e : {
default: e
};
}
var i = Object.assign || function (e) {
for (var t = 1; t < arguments.length; t++) {
var n = arguments[t];
for (var o in n) Object.prototype.hasOwnProperty.call(n, o) && (e[o] = n[o]);
}
return e;
},
r = n(1),
a = (o(r), n(6)),
u = o(a),
c = n(7),
s = o(c),
f = n(8),
d = o(f),
l = n(9),
p = o(l),
m = n(10),
b = o(m),
v = n(11),
y = o(v),
g = n(14),
h = o(g),
w = [],
k = !1,
x = {
offset: 120,
delay: 0,
easing: "ease",
duration: 400,
disable: !1,
once: !1,
startEvent: "DOMContentLoaded",
throttleDelay: 99,
debounceDelay: 50,
disableMutationObserver: !1
},
j = function () {
var e = arguments.length > 0 && void 0 !== arguments[0] && arguments[0];
if (e && (k = !0), k) return w = (0, y.default)(w, x), (0, b.default)(w, x.once), w;
},
O = function () {
w = (0, h.default)(), j();
},
M = function () {
w.forEach(function (e, t) {
e.node.removeAttribute("data-aos"), e.node.removeAttribute("data-aos-easing"), e.node.removeAttribute("data-aos-duration"), e.node.removeAttribute("data-aos-delay");
});
},
S = function (e) {
return e === !0 || "mobile" === e && p.default.mobile() || "phone" === e && p.default.phone() || "tablet" === e && p.default.tablet() || "function" == typeof e && e() === !0;
},
_ = function (e) {
x = i(x, e), w = (0, h.default)();
var t = document.all && !window.atob;
return S(x.disable) || t ? M() : (x.disableMutationObserver || d.default.isSupported() || (console.info('\n aos: MutationObserver is not supported on this browser,\n code mutations observing has been disabled.\n You may have to call "refreshHard()" by yourself.\n '), x.disableMutationObserver = !0), document.querySelector("body").setAttribute("data-aos-easing", x.easing), document.querySelector("body").setAttribute("data-aos-duration", x.duration), document.querySelector("body").setAttribute("data-aos-delay", x.delay), "DOMContentLoaded" === x.startEvent && ["complete", "interactive"].indexOf(document.readyState) > -1 ? j(!0) : "load" === x.startEvent ? window.addEventListener(x.startEvent, function () {
j(!0);
}) : document.addEventListener(x.startEvent, function () {
j(!0);
}), window.addEventListener("resize", (0, s.default)(j, x.debounceDelay, !0)), window.addEventListener("orientationchange", (0, s.default)(j, x.debounceDelay, !0)), window.addEventListener("scroll", (0, u.default)(function () {
(0, b.default)(w, x.once);
}, x.throttleDelay)), x.disableMutationObserver || d.default.ready("[data-aos]", O), w);
};
e.exports = {
init: _,
refresh: j,
refreshHard: O
};
}, function (e, t) {},,,,, function (e, t) {
(function (t) {
"use strict";
function n(e, t, n) {
function o(t) {
var n = b,
o = v;
return b = v = void 0, k = t, g = e.apply(o, n);
}
function r(e) {
return k = e, h = setTimeout(f, t), M ? o(e) : g;
}
function a(e) {
var n = e - w,
o = e - k,
i = t - n;
return S ? j(i, y - o) : i;
}
function c(e) {
var n = e - w,
o = e - k;
return void 0 === w || n >= t || n < 0 || S && o >= y;
}
function f() {
var e = O();
return c(e) ? d(e) : void (h = setTimeout(f, a(e)));
}
function d(e) {
return h = void 0, _ && b ? o(e) : (b = v = void 0, g);
}
function l() {
void 0 !== h && clearTimeout(h), k = 0, b = w = v = h = void 0;
}
function p() {
return void 0 === h ? g : d(O());
}
function m() {
var e = O(),
n = c(e);
if (b = arguments, v = this, w = e, n) {
if (void 0 === h) return r(w);
if (S) return h = setTimeout(f, t), o(w);
}
return void 0 === h && (h = setTimeout(f, t)), g;
}
var b,
v,
y,
g,
h,
w,
k = 0,
M = !1,
S = !1,
_ = !0;
if ("function" != typeof e) throw new TypeError(s);
return t = u(t) || 0, i(n) && (M = !!n.leading, S = "maxWait" in n, y = S ? x(u(n.maxWait) || 0, t) : y, _ = "trailing" in n ? !!n.trailing : _), m.cancel = l, m.flush = p, m;
}
function o(e, t, o) {
var r = !0,
a = !0;
if ("function" != typeof e) throw new TypeError(s);
return i(o) && (r = "leading" in o ? !!o.leading : r, a = "trailing" in o ? !!o.trailing : a), n(e, t, {
leading: r,
maxWait: t,
trailing: a
});
}
function i(e) {
var t = "undefined" == typeof e ? "undefined" : c(e);
return !!e && ("object" == t || "function" == t);
}
function r(e) {
return !!e && "object" == ("undefined" == typeof e ? "undefined" : c(e));
}
function a(e) {
return "symbol" == ("undefined" == typeof e ? "undefined" : c(e)) || r(e) && k.call(e) == d;
}
function u(e) {
if ("number" == typeof e) return e;
if (a(e)) return f;
if (i(e)) {
var t = "function" == typeof e.valueOf ? e.valueOf() : e;
e = i(t) ? t + "" : t;
}
if ("string" != typeof e) return 0 === e ? e : +e;
e = e.replace(l, "");
var n = m.test(e);
return n || b.test(e) ? v(e.slice(2), n ? 2 : 8) : p.test(e) ? f : +e;
}
var c = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (e) {
return typeof e;
} : function (e) {
return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e;
},
s = "Expected a function",
f = NaN,
d = "[object Symbol]",
l = /^\s+|\s+$/g,
p = /^[-+]0x[0-9a-f]+$/i,
m = /^0b[01]+$/i,
b = /^0o[0-7]+$/i,
v = parseInt,
y = "object" == ("undefined" == typeof t ? "undefined" : c(t)) && t && t.Object === Object && t,
g = "object" == ("undefined" == typeof self ? "undefined" : c(self)) && self && self.Object === Object && self,
h = y || g || Function("return this")(),
w = Object.prototype,
k = w.toString,
x = Math.max,
j = Math.min,
O = function () {
return h.Date.now();
};
e.exports = o;
}).call(t, function () {
return this;
}());
}, function (e, t) {
(function (t) {
"use strict";
function n(e, t, n) {
function i(t) {
var n = b,
o = v;
return b = v = void 0, O = t, g = e.apply(o, n);
}
function r(e) {
return O = e, h = setTimeout(f, t), M ? i(e) : g;
}
function u(e) {
var n = e - w,
o = e - O,
i = t - n;
return S ? x(i, y - o) : i;
}
function s(e) {
var n = e - w,
o = e - O;
return void 0 === w || n >= t || n < 0 || S && o >= y;
}
function f() {
var e = j();
return s(e) ? d(e) : void (h = setTimeout(f, u(e)));
}
function d(e) {
return h = void 0, _ && b ? i(e) : (b = v = void 0, g);
}
function l() {
void 0 !== h && clearTimeout(h), O = 0, b = w = v = h = void 0;
}
function p() {
return void 0 === h ? g : d(j());
}
function m() {
var e = j(),
n = s(e);
if (b = arguments, v = this, w = e, n) {
if (void 0 === h) return r(w);
if (S) return h = setTimeout(f, t), i(w);
}
return void 0 === h && (h = setTimeout(f, t)), g;
}
var b,
v,
y,
g,
h,
w,
O = 0,
M = !1,
S = !1,
_ = !0;
if ("function" != typeof e) throw new TypeError(c);
return t = a(t) || 0, o(n) && (M = !!n.leading, S = "maxWait" in n, y = S ? k(a(n.maxWait) || 0, t) : y, _ = "trailing" in n ? !!n.trailing : _), m.cancel = l, m.flush = p, m;
}
function o(e) {
var t = "undefined" == typeof e ? "undefined" : u(e);
return !!e && ("object" == t || "function" == t);
}
function i(e) {
return !!e && "object" == ("undefined" == typeof e ? "undefined" : u(e));
}
function r(e) {
return "symbol" == ("undefined" == typeof e ? "undefined" : u(e)) || i(e) && w.call(e) == f;
}
function a(e) {
if ("number" == typeof e) return e;
if (r(e)) return s;
if (o(e)) {
var t = "function" == typeof e.valueOf ? e.valueOf() : e;
e = o(t) ? t + "" : t;
}
if ("string" != typeof e) return 0 === e ? e : +e;
e = e.replace(d, "");
var n = p.test(e);
return n || m.test(e) ? b(e.slice(2), n ? 2 : 8) : l.test(e) ? s : +e;
}
var u = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (e) {
return typeof e;
} : function (e) {
return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e;
},
c = "Expected a function",
s = NaN,
f = "[object Symbol]",
d = /^\s+|\s+$/g,
l = /^[-+]0x[0-9a-f]+$/i,
p = /^0b[01]+$/i,
m = /^0o[0-7]+$/i,
b = parseInt,
v = "object" == ("undefined" == typeof t ? "undefined" : u(t)) && t && t.Object === Object && t,
y = "object" == ("undefined" == typeof self ? "undefined" : u(self)) && self && self.Object === Object && self,
g = v || y || Function("return this")(),
h = Object.prototype,
w = h.toString,
k = Math.max,
x = Math.min,
j = function () {
return g.Date.now();
};
e.exports = n;
}).call(t, function () {
return this;
}());
}, function (e, t) {
"use strict";
function n(e) {
var t = void 0,
o = void 0,
i = void 0;
for (t = 0; t < e.length; t += 1) {
if (o = e[t], o.dataset && o.dataset.aos) return !0;
if (i = o.children && n(o.children)) return !0;
}
return !1;
}
function o() {
return window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
}
function i() {
return !!o();
}
function r(e, t) {
var n = window.document,
i = o(),
r = new i(a);
u = t, r.observe(n.documentElement, {
childList: !0,
subtree: !0,
removedNodes: !0
});
}
function a(e) {
e && e.forEach(function (e) {
var t = Array.prototype.slice.call(e.addedNodes),
o = Array.prototype.slice.call(e.removedNodes),
i = t.concat(o);
if (n(i)) return u();
});
}
Object.defineProperty(t, "__esModule", {
value: !0
});
var u = function () {};
t.default = {
isSupported: i,
ready: r
};
}, function (e, t) {
"use strict";
function n(e, t) {
if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function");
}
function o() {
return navigator.userAgent || navigator.vendor || window.opera || "";
}
Object.defineProperty(t, "__esModule", {
value: !0
});
var i = function () {
function e(e, t) {
for (var n = 0; n < t.length; n++) {
var o = t[n];
o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, o.key, o);
}
}
return function (t, n, o) {
return n && e(t.prototype, n), o && e(t, o), t;
};
}(),
r = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i,
a = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i,
u = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i,
c = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i,
s = function () {
function e() {
n(this, e);
}
return i(e, [{
key: "phone",
value: function () {
var e = o();
return !(!r.test(e) && !a.test(e.substr(0, 4)));
}
}, {
key: "mobile",
value: function () {
var e = o();
return !(!u.test(e) && !c.test(e.substr(0, 4)));
}
}, {
key: "tablet",
value: function () {
return this.mobile() && !this.phone();
}
}]), e;
}();
t.default = new s();
}, function (e, t) {
"use strict";
Object.defineProperty(t, "__esModule", {
value: !0
});
var n = function (e, t, n) {
var o = e.node.getAttribute("data-aos-once");
t > e.position ? e.node.classList.add("aos-animate") : "undefined" != typeof o && ("false" === o || !n && "true" !== o) && e.node.classList.remove("aos-animate");
},
o = function (e, t) {
var o = window.pageYOffset,
i = window.innerHeight;
e.forEach(function (e, r) {
n(e, i + o, t);
});
};
t.default = o;
}, function (e, t, n) {
"use strict";
function o(e) {
return e && e.__esModule ? e : {
default: e
};
}
Object.defineProperty(t, "__esModule", {
value: !0
});
var i = n(12),
r = o(i),
a = function (e, t) {
return e.forEach(function (e, n) {
e.node.classList.add("aos-init"), e.position = (0, r.default)(e.node, t.offset);
}), e;
};
t.default = a;
}, function (e, t, n) {
"use strict";
function o(e) {
return e && e.__esModule ? e : {
default: e
};
}
Object.defineProperty(t, "__esModule", {
value: !0
});
var i = n(13),
r = o(i),
a = function (e, t) {
var n = 0,
o = 0,
i = window.innerHeight,
a = {
offset: e.getAttribute("data-aos-offset"),
anchor: e.getAttribute("data-aos-anchor"),
anchorPlacement: e.getAttribute("data-aos-anchor-placement")
};
switch (a.offset && !isNaN(a.offset) && (o = parseInt(a.offset)), a.anchor && document.querySelectorAll(a.anchor) && (e = document.querySelectorAll(a.anchor)[0]), n = (0, r.default)(e).top, a.anchorPlacement) {
case "top-bottom":
break;
case "center-bottom":
n += e.offsetHeight / 2;
break;
case "bottom-bottom":
n += e.offsetHeight;
break;
case "top-center":
n += i / 2;
break;
case "bottom-center":
n += i / 2 + e.offsetHeight;
break;
case "center-center":
n += i / 2 + e.offsetHeight / 2;
break;
case "top-top":
n += i;
break;
case "bottom-top":
n += e.offsetHeight + i;
break;
case "center-top":
n += e.offsetHeight / 2 + i;
}
return a.anchorPlacement || a.offset || isNaN(t) || (o = t), n + o;
};
t.default = a;
}, function (e, t) {
"use strict";
Object.defineProperty(t, "__esModule", {
value: !0
});
var n = function (e) {
for (var t = 0, n = 0; e && !isNaN(e.offsetLeft) && !isNaN(e.offsetTop);) t += e.offsetLeft - ("BODY" != e.tagName ? e.scrollLeft : 0), n += e.offsetTop - ("BODY" != e.tagName ? e.scrollTop : 0), e = e.offsetParent;
return {
top: n,
left: t
};
};
t.default = n;
}, function (e, t) {
"use strict";
Object.defineProperty(t, "__esModule", {
value: !0
});
var n = function (e) {
return e = e || document.querySelectorAll("[data-aos]"), Array.prototype.map.call(e, function (e) {
return {
node: e
};
});
};
t.default = n;
}]);
});
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/************************************************************************/
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
(() => { (() => {
"use strict";
// UNUSED EXPORTS: default // UNUSED EXPORTS: default
@@ -1320,9 +724,6 @@ var BasePage = /*#__PURE__*/function () {
}]); }]);
}(); }();
// EXTERNAL MODULE: ./node_modules/aos/dist/aos.js
var aos = __webpack_require__(431);
var aos_default = /*#__PURE__*/__webpack_require__.n(aos);
;// ./static/js/pages/core/home.js ;// ./static/js/pages/core/home.js
function home_typeof(o) { "@babel/helpers - typeof"; return home_typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, home_typeof(o); } function home_typeof(o) { "@babel/helpers - typeof"; return home_typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, home_typeof(o); }
function home_classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function home_classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
@@ -1344,7 +745,6 @@ function home_toPrimitive(t, r) { if ("object" != home_typeof(t) || !t) return t
// internal // internal
// external // external
var PageHome = /*#__PURE__*/function (_BasePage) { var PageHome = /*#__PURE__*/function (_BasePage) {
function PageHome(router) { function PageHome(router) {
home_classCallCheck(this, PageHome); home_classCallCheck(this, PageHome);
@@ -1356,45 +756,7 @@ var PageHome = /*#__PURE__*/function (_BasePage) {
value: function initialize() { value: function initialize() {
this.sharedInitialize(); this.sharedInitialize();
this.hookupButtonsNavContact(); this.hookupButtonsNavContact();
// this.initialiseAOS();
this.initialiseAnimations();
} }
/* AOS */
}, {
key: "initialiseAOS",
value: function initialiseAOS() {
aos_default().init({
duration: 1000,
once: true
});
}
/* Manual animations *
initialiseAnimations() {
// Check if IntersectionObserver is supported
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
}
});
}, {
threshold: 0.1,
rootMargin: '50px'
});
// Observe all elements with 'reveal' class
document.querySelectorAll('.reveal').forEach((element) => {
observer.observe(element);
});
} else {
// If IntersectionObserver is not supported, make all elements visible
document.querySelectorAll('.reveal').forEach((element) => {
element.style.opacity = 1;
});
}
}
*/
}, { }, {
key: "leave", key: "leave",
value: function leave() { value: function leave() {
@@ -1430,8 +792,32 @@ var PageContact = /*#__PURE__*/function (_BasePage) {
key: "initialize", key: "initialize",
value: function initialize() { value: function initialize() {
this.sharedInitialize(); this.sharedInitialize();
this.hookupCaptcha();
this.hookupButtonSubmitFormContactUs(); this.hookupButtonSubmitFormContactUs();
} }
}, {
key: "hookupCaptcha",
value: function hookupCaptcha() {
var form = document.querySelector(idContactForm);
var altchaWidget = form.querySelector('altcha-widget');
// Listen for verification events from the ALTCHA widget
if (altchaWidget) {
altchaWidget.addEventListener('serververification', function (event) {
// Create or update the hidden input for ALTCHA
var altchaInput = form.querySelector('input[name="altcha"]');
if (!altchaInput) {
altchaInput = document.createElement('input');
altchaInput.type = 'hidden';
altchaInput.name = 'altcha';
form.appendChild(altchaInput);
}
// Set the verification payload
altchaInput.value = event.detail.payload;
});
}
}
}, { }, {
key: "hookupButtonSubmitFormContactUs", key: "hookupButtonSubmitFormContactUs",
value: function hookupButtonSubmitFormContactUs() { value: function hookupButtonSubmitFormContactUs() {
@@ -2301,65 +1687,56 @@ window.app = app;
/* harmony default export */ const js_app = ((/* unused pure expression or super */ null && (app))); /* harmony default export */ const js_app = ((/* unused pure expression or super */ null && (app)));
})(); })();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode. // This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => { (() => {
"use strict";
// extracted by mini-css-extract-plugin // extracted by mini-css-extract-plugin
})(); })();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode. // This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => { (() => {
"use strict";
// extracted by mini-css-extract-plugin // extracted by mini-css-extract-plugin
})(); })();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode. // This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => { (() => {
"use strict";
// extracted by mini-css-extract-plugin // extracted by mini-css-extract-plugin
})(); })();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode. // This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => { (() => {
"use strict";
// extracted by mini-css-extract-plugin // extracted by mini-css-extract-plugin
})(); })();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode. // This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => { (() => {
"use strict";
// extracted by mini-css-extract-plugin // extracted by mini-css-extract-plugin
})(); })();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode. // This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => { (() => {
"use strict";
// extracted by mini-css-extract-plugin // extracted by mini-css-extract-plugin
})(); })();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode. // This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => { (() => {
"use strict";
// extracted by mini-css-extract-plugin // extracted by mini-css-extract-plugin
})(); })();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode. // This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => { (() => {
"use strict";
// extracted by mini-css-extract-plugin // extracted by mini-css-extract-plugin
})(); })();
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode. // This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => { (() => {
"use strict";
// extracted by mini-css-extract-plugin // extracted by mini-css-extract-plugin
})(); })();

View File

@@ -10,9 +10,32 @@ export default class PageContact extends BasePage {
initialize() { initialize() {
this.sharedInitialize(); this.sharedInitialize();
this.hookupCaptcha();
this.hookupButtonSubmitFormContactUs(); this.hookupButtonSubmitFormContactUs();
} }
hookupCaptcha() {
const form = document.querySelector(idContactForm);
const altchaWidget = form.querySelector('altcha-widget');
// Listen for verification events from the ALTCHA widget
if (altchaWidget) {
altchaWidget.addEventListener('serververification', function(event) {
// Create or update the hidden input for ALTCHA
let altchaInput = form.querySelector('input[name="altcha"]');
if (!altchaInput) {
altchaInput = document.createElement('input');
altchaInput.type = 'hidden';
altchaInput.name = 'altcha';
form.appendChild(altchaInput);
}
// Set the verification payload
altchaInput.value = event.detail.payload;
});
}
}
hookupButtonSubmitFormContactUs() { hookupButtonSubmitFormContactUs() {
const button = document.querySelector('form input[type="submit"]'); const button = document.querySelector('form input[type="submit"]');
button.classList.add(flagButton); button.classList.add(flagButton);

View File

@@ -2,7 +2,6 @@
// internal // internal
import BasePage from "../base.js"; import BasePage from "../base.js";
// external // external
import AOS from 'aos';
export default class PageHome extends BasePage { export default class PageHome extends BasePage {
@@ -15,46 +14,10 @@ export default class PageHome extends BasePage {
initialize() { initialize() {
this.sharedInitialize(); this.sharedInitialize();
this.hookupButtonsNavContact(); this.hookupButtonsNavContact();
// this.initialiseAOS();
this.initialiseAnimations();
} }
/* AOS */
initialiseAOS() {
AOS.init({
duration: 1000,
once: true,
});
}
/* Manual animations *
initialiseAnimations() {
// Check if IntersectionObserver is supported
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
}
});
}, {
threshold: 0.1,
rootMargin: '50px'
});
// Observe all elements with 'reveal' class
document.querySelectorAll('.reveal').forEach((element) => {
observer.observe(element);
});
} else {
// If IntersectionObserver is not supported, make all elements visible
document.querySelectorAll('.reveal').forEach((element) => {
element.style.opacity = 1;
});
}
}
*/
leave() { leave() {
super.leave(); super.leave();
} }
} }

2636
static/js/vendor/altcha.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -117,7 +117,7 @@
var flagPageBody = "{{ model.FLAG_PAGE_BODY }}"; var flagPageBody = "{{ model.FLAG_PAGE_BODY }}";
var flagPhoneNumber = "{{ model.FLAG_PHONE_NUMBER }}"; var flagPhoneNumber = "{{ model.FLAG_PHONE_NUMBER }}";
var flagPostcode = "{{ model.FLAG_POSTCODE }}"; var flagPostcode = "{{ model.FLAG_POSTCODE }}";
var flagRecaptcha = "{{ model.FLAG_RECAPTCHA }}"; var flagCaptcha = "{{ model.FLAG_CAPTCHA }}";
var flagRightHandSide = "{{ model.FLAG_RIGHT_HAND_SIDE }}"; var flagRightHandSide = "{{ model.FLAG_RIGHT_HAND_SIDE }}";
var flagRow = "{{ model.FLAG_ROW }}"; var flagRow = "{{ model.FLAG_ROW }}";
var flagRowNew = "{{ model.FLAG_ROW_NEW }}"; var flagRowNew = "{{ model.FLAG_ROW_NEW }}";

View File

@@ -2,6 +2,10 @@
{% block page_head %} {% block page_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='dist/css/core_contact.bundle.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='dist/css/core_contact.bundle.css') }}">
{#
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@altcha/browser@latest/dist/index.js" defer></script>
#}
<script type="module" src="{{ url_for('static', filename='js/vendor/altcha.js')}}"></script>
{% endblock %} {% endblock %}
{% block page_nav_links %} {% block page_nav_links %}
@@ -12,6 +16,7 @@
{% endblock %} {% endblock %}
{% block page_body %} {% block page_body %}
{#
<script> <script>
function loadRecaptcha() { function loadRecaptcha() {
var script = document.createElement('script'); var script = document.createElement('script');
@@ -22,15 +27,15 @@
window.addEventListener('load', loadRecaptcha); window.addEventListener('load', loadRecaptcha);
</script> </script>
#}
<!-- Divs -->
{% set form = model.form_contact %} {% set form = model.form_contact %}
<section class="contact-section"> <section class="contact-section">
<div class="contact-form"> <div class="contact-form">
<h1>Contact Us</h1> <h1>Contact Us</h1>
<p>Please fill in the form below and we'll get back to you as soon as possible.</p> <p>Please fill in the form below and we'll get back to you as soon as possible.</p>
<form id="contact-form" method="POST" action="{{ url_for('routes_core.contact') }}"> <form id="{{ model.ID_CONTACT_FORM }}" method="POST" action="{{ url_for('routes_core.contact') }}">
{{ form.csrf_token }} {{ form.csrf_token }}
<div class="form-grid"> <div class="form-grid">
@@ -59,8 +64,12 @@
{{ model.form_contact.receive_marketing() }} {{ model.form_contact.receive_marketing() }}
{{ model.form_contact.receive_marketing.label }} {{ model.form_contact.receive_marketing.label }}
</div> </div>
<div class="{{ model.FLAG_CONTAINER }} {{ model.FLAG_RECAPTCHA }}"> <div class="{{ model.FLAG_CONTAINER }} {{ model.FLAG_CAPTCHA }}">
{{ model.form_contact.recaptcha() }} {# {{ model.form_contact.recaptcha() }} #}
<altcha-widget
challengeurl="https://eu.altcha.org/api/v1/challenge?apiKey={{ model.app.app_config.ALTCHA_API_KEY }}"
spamfilter
></altcha-widget>
</div> </div>
<div class="{{ model.FLAG_CONTAINER_INPUT }}"> <div class="{{ model.FLAG_CONTAINER_INPUT }}">
{{ model.form_contact.submit() }} {{ model.form_contact.submit() }}
@@ -101,6 +110,7 @@
#} #}
<script> <script>
var idContactForm = "#{{ model.ID_CONTACT_FORM }}";
var idEmail = "#{{ model.ID_EMAIL }}"; var idEmail = "#{{ model.ID_EMAIL }}";
var idMessage = "#{{ model.ID_MESSAGE }}"; var idMessage = "#{{ model.ID_MESSAGE }}";
var idContactName = "#{{ model.ID_CONTACT_NAME }}"; var idContactName = "#{{ model.ID_CONTACT_NAME }}";